KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt

216 lines
6.2 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.io
import ru.dbotthepony.kstarbound.api.IVFS
import java.io.*
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.util.*
import kotlin.collections.HashMap
private fun readHeader(reader: RandomAccessFile, required: Int) {
val read = reader.read()
require(read == required) { "Bad Starbound Pak header, expected $required, got $read" }
}
class StarboundPakFile(
val storage: StarboundPak,
val name: String,
val offset: Long,
val length: Long
) {
val directoryName: String
val directoryHiearchy: Array<String>
val fileName: String
init {
val split = name.substring(1).split('/').toMutableList()
fileName = split.last()
split.removeAt(split.size - 1)
directoryHiearchy = split.toTypedArray()
directoryName = split.joinToString("/")
}
fun read(): ByteBuffer {
val buf = ByteBuffer.allocate(length.toInt())
storage.reader.seek(offset)
storage.reader.readFully(buf.array())
return buf
}
override fun toString(): String {
return name
}
fun readString(): String {
return read().array().toString(Charsets.UTF_8)
}
companion object {
fun read(storage: StarboundPak, reader: RandomAccessFile): StarboundPakFile {
val readLength = reader.read()
//val name = reader.readASCIIString(readLength).lowercase()
val name = reader.readASCIIString(readLength)
require(name[0] == '/') { "$name appears to be not an absolute filename" }
val offset = reader.readLong()
val length = reader.readLong()
return StarboundPakFile(storage, name.intern(), offset, length)
}
fun read(storage: StarboundPak, reader: DataInputStream): StarboundPakFile {
val readLength = reader.readVarInt()
val name = reader.readASCIIString(readLength)
require(name[0] == '/') { "$name appears to be not an absolute filename" }
val offset = reader.readLong()
val length = reader.readLong()
return StarboundPakFile(storage, name.intern(), offset, length)
}
}
}
class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) {
val files = HashMap<String, StarboundPakFile>()
val directories = HashMap<String, StarboundPakDirectory>()
fun resolve(path: Array<String>, level: Int = 0): StarboundPakDirectory {
if (path.size == level)
return this
if (level == 0 && path[0] == "" && name == "/")
return resolve(path, 1)
if (directories.containsKey(path[level])) {
return directories[path[level]]!!.resolve(path, level + 1)
} else {
val dir = StarboundPakDirectory(path[level], this)
directories[path[level]] = dir
return dir.resolve(path, level + 1)
}
}
fun getFile(name: String) = files[name]
fun getDirectory(name: String) = directories[name]
fun listFiles(): Collection<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
fun listDirectories(): Collection<StarboundPakDirectory> = Collections.unmodifiableCollection(directories.values)
fun writeFile(file: StarboundPakFile) {
files[file.name.split('/').last()] = file
}
fun fullName(): String {
var build = name
var getParent = parent
while (getParent != null) {
if (getParent.parent != null) {
build = "${getParent.name}/$build"
} else {
build = "/$build"
break
}
getParent = getParent.parent
}
return build
}
override fun toString(): String {
return fullName()
}
}
class StarboundPak(val path: File, callback: (finished: Boolean, status: String) -> Unit = { _, _ -> }) : Closeable, IVFS {
val reader = RandomAccessFile(path, "r")
init {
readHeader(reader, 0x53) // S
readHeader(reader, 0x42) // B
readHeader(reader, 0x41) // A
readHeader(reader, 0x73) // s
readHeader(reader, 0x73) // s
readHeader(reader, 0x65) // e
readHeader(reader, 0x74) // t
readHeader(reader, 0x36) // 6
}
// Далее идёт 8 байтный long в формате Big Endian, который указывает на offset до INDEX
// т.е. сделав seek(indexOffset) мы выйдем прямо на INDEX
private val indexOffset = reader.readLong()
init {
reader.seek(indexOffset)
readHeader(reader, 0x49) // I
readHeader(reader, 0x4E) // N
readHeader(reader, 0x44) // D
readHeader(reader, 0x45) // E
readHeader(reader, 0x58) // X
callback(false, "Reading metadata")
}
// сразу за INDEX идут метаданные в формате Binary Json
val metadata = BinaryJson.readObject(reader)
// сразу за метаданными идёт количество файлов внутри данного pak в формате Big Endian variable int
val indexNodeCount = reader.readVarLong()
private val indexNodes = HashMap<String, StarboundPakFile>()
val root = StarboundPakDirectory("/")
init {
// Сразу же за количеством файлов идут сами файлы в формате
// byte (длинна имени файла)
// byte[] (utf-8 имя файла)
// long (offset от начала файла)
// long (длинна файла)
val stream = DataInputStream(BufferedInputStream(Channels.newInputStream(reader.channel)))
for (i in 0 until indexNodeCount) {
try {
callback(false, "Reading index node $i")
val read = StarboundPakFile.read(this, stream)
if (read.offset > reader.length()) {
throw IndexOutOfBoundsException("Garbage offset at index $i: ${read.offset}")
}
if (read.length > reader.length()) {
throw IndexOutOfBoundsException("Garbage length at index $i: ${read.length}")
}
indexNodes[read.name] = read
root.resolve(read.directoryHiearchy).writeFile(read)
} catch (err: Throwable) {
throw IOException("Reading index node at $i", err)
}
}
callback(true, "Reading indexes finished")
}
override fun listFiles(path: String): Collection<String> {
return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name }
}
override fun listDirectories(path: String): Collection<String> {
return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() }
}
override fun pathExists(path: String): Boolean {
return indexNodes.containsKey(path)
}
override fun readOrNull(path: String): ByteBuffer? {
val node = indexNodes[path] ?: return null
return node.read()
}
override fun close() {
reader.close()
}
}