216 lines
6.2 KiB
Kotlin
216 lines
6.2 KiB
Kotlin
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()
|
||
}
|
||
}
|