Open-source multiplayer game server compatible with the RuneScape client https://www.openrs2.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
openrs2/cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt

269 lines
9.6 KiB

package org.openrs2.cache
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import org.bouncycastle.crypto.params.RSAKeyParameters
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import org.openrs2.crypto.Rsa
import org.openrs2.crypto.Whirlpool
import org.openrs2.crypto.rsa
import org.openrs2.crypto.whirlpool
public data class Js5MasterIndex(
public var format: MasterIndexFormat,
public val entries: MutableList<Entry> = mutableListOf()
) {
public class Entry(
public var version: Int,
public var checksum: Int,
public var groups: Int,
public var totalUncompressedLength: Int,
digest: ByteArray?
) {
public var digest: ByteArray? = digest
set(value) {
require(value == null || value.size == Whirlpool.DIGESTBYTES)
field = value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Entry
if (version != other.version) return false
if (checksum != other.checksum) return false
if (groups != other.groups) return false
if (totalUncompressedLength != other.totalUncompressedLength) return false
if (digest != null) {
if (other.digest == null) return false
if (!digest.contentEquals(other.digest)) return false
} else if (other.digest != null) return false
return true
}
override fun hashCode(): Int {
var result = version
result = 31 * result + checksum
result = 31 * result + groups
result = 31 * result + totalUncompressedLength
result = 31 * result + (digest?.contentHashCode() ?: 0)
return result
}
override fun toString(): String {
val digest = digest
val hex = if (digest != null) {
ByteBufUtil.hexDump(digest)
} else {
"null"
}
return "Entry(version=$version, checksum=$checksum, groups=$groups, " +
"totalUncompressedLength=$totalUncompressedLength, digest=$hex)"
}
}
public fun write(buf: ByteBuf, key: RSAKeyParameters? = null) {
val start = buf.writerIndex()
if (format >= MasterIndexFormat.DIGESTS) {
buf.writeByte(entries.size)
}
for (entry in entries) {
buf.writeInt(entry.checksum)
if (format >= MasterIndexFormat.VERSIONED) {
buf.writeInt(entry.version)
}
if (format >= MasterIndexFormat.LENGTHS) {
buf.writeInt(entry.groups)
buf.writeInt(entry.totalUncompressedLength)
}
if (format >= MasterIndexFormat.DIGESTS) {
val digest = entry.digest
if (digest != null) {
buf.writeBytes(digest)
} else {
buf.writeZero(Whirlpool.DIGESTBYTES)
}
}
}
if (format >= MasterIndexFormat.DIGESTS) {
val digest = buf.whirlpool(start, buf.writerIndex() - start)
if (key != null) {
buf.alloc().buffer(SIGNATURE_LENGTH, SIGNATURE_LENGTH).use { plaintext ->
plaintext.writeByte(Rsa.MAGIC)
plaintext.writeBytes(digest)
plaintext.rsa(key).use { ciphertext ->
buf.writeBytes(ciphertext)
}
}
} else {
buf.writeByte(Rsa.MAGIC)
buf.writeBytes(digest)
}
}
}
public companion object {
private const val SIGNATURE_LENGTH = Whirlpool.DIGESTBYTES + 1
public fun create(store: Store): Js5MasterIndex {
val masterIndex = Js5MasterIndex(MasterIndexFormat.ORIGINAL)
var nextArchive = 0
for (archive in store.list(Js5Archive.ARCHIVESET)) {
val entry = try {
store.read(Js5Archive.ARCHIVESET, archive).use { buf ->
val checksum = buf.crc32()
val digest = buf.whirlpool()
Js5Compression.uncompress(buf).use { uncompressed ->
val index = Js5Index.read(uncompressed)
if (index.hasLengths) {
masterIndex.format = maxOf(masterIndex.format, MasterIndexFormat.LENGTHS)
} else if (index.hasDigests) {
masterIndex.format = maxOf(masterIndex.format, MasterIndexFormat.DIGESTS)
} else if (index.protocol >= Js5Protocol.VERSIONED) {
masterIndex.format = maxOf(masterIndex.format, MasterIndexFormat.VERSIONED)
}
val version = index.version
val groups = index.size
val totalUncompressedLength = index.sumOf(Js5Index.Group::uncompressedLength)
// TODO(gpe): should we throw an exception if there are trailing bytes here or in the block above?
Entry(version, checksum, groups, totalUncompressedLength, digest)
}
}
} catch (ex: StoreCorruptException) {
/**
* Unused indexes are never removed from the .idx255 file
* by the client. If the .dat2 file reaches its maximum
* size, it is truncated and all block numbers in the
* .idx255 file will be invalid.
*
* Any in-use indexes will be overwritten, but unused
* indexes will remain in the .idx255 file with invalid
* block numbers.
*
* We therefore expect to see corrupt indexes sometimes. We
* ignore these as if they didn't exist.
*/
continue
}
/*
* Fill in gaps with zeroes. I think this is consistent with
* the official implementation: the TFU client warns that
* entries with a zero CRC are probably invalid.
*/
for (i in nextArchive until archive) {
masterIndex.entries += Entry(0, 0, 0, 0, null)
}
masterIndex.entries += entry
nextArchive = archive + 1
}
return masterIndex
}
public fun read(buf: ByteBuf, format: MasterIndexFormat, key: RSAKeyParameters? = null): Js5MasterIndex {
val index = Js5MasterIndex(format)
val start = buf.readerIndex()
val len = buf.readableBytes()
val archives = when (format) {
MasterIndexFormat.ORIGINAL -> {
require(len % 4 == 0) {
"Length is not a multiple of 4 bytes"
}
len / 4
}
MasterIndexFormat.VERSIONED -> {
require(len % 8 == 0) {
"Length is not a multiple of 8 bytes"
}
len / 8
}
else -> {
buf.readUnsignedByte().toInt()
}
}
for (i in 0 until archives) {
val checksum = buf.readInt()
val version = if (format >= MasterIndexFormat.VERSIONED) {
buf.readInt()
} else {
0
}
val groups: Int
val totalUncompressedLength: Int
if (format >= MasterIndexFormat.LENGTHS) {
groups = buf.readInt()
totalUncompressedLength = buf.readInt()
} else {
groups = 0
totalUncompressedLength = 0
}
val digest = if (format >= MasterIndexFormat.DIGESTS) {
val bytes = ByteArray(Whirlpool.DIGESTBYTES)
buf.readBytes(bytes)
bytes
} else {
null
}
index.entries += Entry(version, checksum, groups, totalUncompressedLength, digest)
}
val end = buf.readerIndex()
if (format >= MasterIndexFormat.DIGESTS) {
val ciphertext = buf.readSlice(buf.readableBytes())
decrypt(ciphertext, key).use { plaintext ->
require(plaintext.readableBytes() == SIGNATURE_LENGTH) {
"Invalid signature length"
}
// the client doesn't verify what I presume is the RSA magic byte
plaintext.skipBytes(1)
val expected = ByteArray(Whirlpool.DIGESTBYTES)
plaintext.readBytes(expected)
val actual = buf.whirlpool(start, end - start)
require(expected.contentEquals(actual)) {
"Invalid signature"
}
}
}
return index
}
private fun decrypt(buf: ByteBuf, key: RSAKeyParameters?): ByteBuf {
return if (key != null) {
buf.rsa(key)
} else {
buf.retain()
}
}
}
}