package dev.openrs2.cache import dev.openrs2.buffer.use import dev.openrs2.crypto.XteaKey import dev.openrs2.crypto.xteaDecrypt import dev.openrs2.crypto.xteaEncrypt import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBufInputStream import io.netty.buffer.ByteBufOutputStream object Js5Compression { fun compress(input: ByteBuf, type: Js5CompressionType, key: XteaKey = XteaKey.ZERO): ByteBuf { input.alloc().buffer().use { output -> output.writeByte(type.ordinal) if (type == Js5CompressionType.NONE) { val len = input.readableBytes() output.writeInt(len) output.writeBytes(input) if (!key.isZero) { output.xteaEncrypt(5, len, key) } return output.retain() } val lenIndex = output.writerIndex() output.writeZero(4) output.writeInt(input.readableBytes()) val start = output.writerIndex() type.createOutputStream(ByteBufOutputStream(output)).use { outputStream -> ByteBufInputStream(input).use { inputStream -> inputStream.copyTo(outputStream) } } val len = output.writerIndex() - start output.setInt(lenIndex, len) if (!key.isZero) { output.xteaEncrypt(5, len + 4, key) } return output.retain() } } fun compressBest(input: ByteBuf, enableLzma: Boolean = false, key: XteaKey = XteaKey.ZERO): ByteBuf { var best = compress(input.slice(), Js5CompressionType.NONE, key) try { for (type in Js5CompressionType.values()) { if (type == Js5CompressionType.NONE || (type == Js5CompressionType.LZMA && !enableLzma)) { continue } compress(input.slice(), type, key).use { output -> if (output.readableBytes() < best.readableBytes()) { best.release() best = output.retain() } } } // consume all of input so this method is a drop-in replacement for compress() input.skipBytes(input.readableBytes()) return best.retain() } finally { best.release() } } fun uncompress(input: ByteBuf, key: XteaKey = XteaKey.ZERO): ByteBuf { val typeId = input.readUnsignedByte().toInt() val type = Js5CompressionType.fromOrdinal(typeId) require(type != null) { "Invalid compression type: $typeId" } val len = input.readInt() require(len >= 0) { "Length is negative: $len" } if (type == Js5CompressionType.NONE) { input.readBytes(len).use { output -> if (!key.isZero) { output.xteaDecrypt(0, len, key) } return output.retain() } } decrypt(input, len + 4, key).use { plaintext -> val uncompressedLen = plaintext.readInt() require(uncompressedLen >= 0) { "Uncompressed length is negative: $uncompressedLen" } plaintext.alloc().buffer(uncompressedLen, uncompressedLen).use { output -> type.createInputStream(ByteBufInputStream(plaintext, len), uncompressedLen).use { inputStream -> ByteBufOutputStream(output).use { outputStream -> inputStream.copyTo(outputStream) } } return output.retain() } } } private fun decrypt(buf: ByteBuf, len: Int, key: XteaKey): ByteBuf { if (key.isZero) { return buf.readRetainedSlice(len) } buf.readBytes(len).use { output -> output.xteaDecrypt(0, len, key) return output.retain() } } }