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 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() val directories = HashMap() fun resolve(path: Array, 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 = Collections.unmodifiableCollection(files.values) fun listDirectories(): Collection = 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() 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 { return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name } } override fun listDirectories(path: String): Collection { 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() } }