From aa2784a9e611db65c408547cad7436a60e52410d Mon Sep 17 00:00:00 2001 From: Graham Date: Thu, 31 Mar 2022 21:38:07 +0100 Subject: [PATCH] Add class for converting RuneLite flatcaches to other formats Signed-off-by: Graham --- .../kotlin/org/openrs2/cache/RuneLiteStore.kt | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 cache/src/main/kotlin/org/openrs2/cache/RuneLiteStore.kt diff --git a/cache/src/main/kotlin/org/openrs2/cache/RuneLiteStore.kt b/cache/src/main/kotlin/org/openrs2/cache/RuneLiteStore.kt new file mode 100644 index 00000000..137ae1d5 --- /dev/null +++ b/cache/src/main/kotlin/org/openrs2/cache/RuneLiteStore.kt @@ -0,0 +1,162 @@ +package org.openrs2.cache + +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.Unpooled +import org.openrs2.buffer.crc32 +import org.openrs2.buffer.use +import java.nio.file.Files +import java.nio.file.Path +import java.util.Base64 +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.io.path.name + +@Singleton +public class RuneLiteStore @Inject constructor( + private val alloc: ByteBufAllocator +) { + public fun unpack(input: Path, output: Store) { + output.create(Store.ARCHIVESET) + + for (path in Files.list(input)) { + val name = path.name + if (!name.endsWith(".flatcache")) { + continue + } + + val archive = name.removeSuffix(".flatcache").toIntOrNull() ?: continue + unpackArchive(path, archive, output) + } + } + + private fun unpackArchive(path: Path, archive: Int, output: Store) { + val index = Js5Index(Js5Protocol.ORIGINAL) + var indexChecksum = 0 + + Files.newBufferedReader(path).useLines { lines -> + var group: Js5Index.MutableGroup? = null + + for (line in lines) { + val pair = line.split('=', limit = 2) + if (pair.size != 2) { + throw StoreCorruptException("Missing = in line") + } + + val (key, value) = pair + + if (group == null) { + when (key) { + "protocol" -> { + val protocolId = value.toIntOrNull() + ?: throw StoreCorruptException("Protocol must be an integer") + + index.protocol = Js5Protocol.fromId(protocolId) + ?: throw StoreCorruptException("Protocol number not supported") + } + + "revision" -> { + index.version = value.toIntOrNull() + ?: throw StoreCorruptException("Revision must be an integer") + } + + "compression" -> Unit + + "crc" -> { + indexChecksum = value.toIntOrNull() + ?: throw StoreCorruptException("Index CRC must be an integer") + } + + "named" -> Unit + + "id" -> { + val id = value.toIntOrNull() + ?: throw StoreCorruptException("Group ID must be an integer") + + group = index.createOrGet(id) + } + + else -> throw StoreCorruptException("Unknown key in archive context: $key") + } + } else { + when (key) { + "namehash" -> { + group.nameHash = value.toIntOrNull() + ?: throw StoreCorruptException("Group name hash must be an integer") + + if (group.nameHash != 0) { + index.hasNames = true + } + } + + "revision" -> { + group.version = value.toIntOrNull() + ?: throw StoreCorruptException("Revision must be an integer") + } + + "crc" -> { + group.checksum = value.toIntOrNull() + ?: throw StoreCorruptException("Group CRC must be an integer") + } + + "contents" -> { + Unpooled.wrappedBuffer(Base64.getDecoder().decode(value)).use { buf -> + output.write(archive, group!!.id, buf) + } + } + + "compression" -> Unit + + "file" -> { + val pair = value.split('=', limit = 2) + if (pair.size != 2) { + throw StoreCorruptException("Missing = in file line") + } + + val id = pair[0].toIntOrNull() + ?: throw StoreCorruptException("File ID must be an integer") + + val file = group.createOrGet(id) + + file.nameHash = pair[1].toIntOrNull() + ?: throw StoreCorruptException("File name hash must be an integer") + + if (file.nameHash != 0) { + index.hasNames = true + } + } + + "id" -> { + val id = value.toIntOrNull() + ?: throw StoreCorruptException("Group ID must be an integer") + + group = index.createOrGet(id) + } + + else -> throw StoreCorruptException("Unknown key in group context: $key") + } + } + } + } + + alloc.buffer().use { uncompressed -> + index.write(uncompressed) + + val matching = Js5CompressionType.values().count { type -> + Js5Compression.compress(uncompressed.slice(), type).use { compressed -> + val checksum = compressed.crc32() + + if (checksum == indexChecksum) { + output.write(Store.ARCHIVESET, archive, compressed) + return@use true + } + + return@use false + } + } + + if (matching != 1) { + throw StoreCorruptException("Failed to reconstruct Js5Index") + } + } + } +}