From 5036eb3da8b38a620a80f632be7542f46f54c8aa Mon Sep 17 00:00:00 2001 From: Graham Date: Sun, 25 Oct 2020 11:02:17 +0000 Subject: [PATCH] Add JS5 remote protocol implementation Signed-off-by: Graham --- protocol/build.gradle.kts | 28 ++++ .../org/openrs2/protocol/js5/Js5Request.kt | 15 ++ .../openrs2/protocol/js5/Js5RequestDecoder.kt | 35 +++++ .../openrs2/protocol/js5/Js5RequestEncoder.kt | 44 ++++++ .../org/openrs2/protocol/js5/Js5Response.kt | 43 ++++++ .../protocol/js5/Js5ResponseDecoder.kt | 97 ++++++++++++ .../protocol/js5/Js5ResponseEncoder.kt | 45 ++++++ .../org/openrs2/protocol/js5/XorDecoder.kt | 13 ++ .../org/openrs2/protocol/js5/XorEncoder.kt | 13 ++ .../org/openrs2/protocol/js5/XorExtensions.kt | 35 +++++ .../protocol/js5/Js5RequestDecoderTest.kt | 45 ++++++ .../protocol/js5/Js5RequestEncoderTest.kt | 32 ++++ .../protocol/js5/Js5ResponseDecoderTest.kt | 138 ++++++++++++++++++ .../protocol/js5/Js5ResponseEncoderTest.kt | 60 ++++++++ .../openrs2/protocol/js5/XorDecoderTest.kt | 40 +++++ .../openrs2/protocol/js5/XorEncoderTest.kt | 40 +++++ .../openrs2/protocol/js5/1019-prefetch.dat | Bin 0 -> 1023 bytes .../org/openrs2/protocol/js5/1019-urgent.dat | Bin 0 -> 1023 bytes .../org/openrs2/protocol/js5/1019.dat | Bin 0 -> 1019 bytes .../openrs2/protocol/js5/1020-prefetch.dat | Bin 0 -> 1025 bytes .../org/openrs2/protocol/js5/1020-urgent.dat | Bin 0 -> 1025 bytes .../org/openrs2/protocol/js5/1020.dat | Bin 0 -> 1020 bytes .../openrs2/protocol/js5/1530-prefetch.dat | Bin 0 -> 1535 bytes .../org/openrs2/protocol/js5/1530-urgent.dat | Bin 0 -> 1535 bytes .../org/openrs2/protocol/js5/1530.dat | Bin 0 -> 1530 bytes .../openrs2/protocol/js5/1531-prefetch.dat | Bin 0 -> 1537 bytes .../org/openrs2/protocol/js5/1531-urgent.dat | Bin 0 -> 1537 bytes .../org/openrs2/protocol/js5/1531.dat | Bin 0 -> 1531 bytes .../org/openrs2/protocol/js5/508-prefetch.dat | Bin 0 -> 511 bytes .../org/openrs2/protocol/js5/508-urgent.dat | Bin 0 -> 511 bytes .../org/openrs2/protocol/js5/508.dat | Bin 0 -> 508 bytes .../org/openrs2/protocol/js5/509-prefetch.dat | Bin 0 -> 513 bytes .../org/openrs2/protocol/js5/509-urgent.dat | Bin 0 -> 513 bytes .../org/openrs2/protocol/js5/509.dat | Bin 0 -> 509 bytes .../protocol/js5/invalid-block-trailer.dat | Bin 0 -> 513 bytes settings.gradle.kts | 1 + 36 files changed, 724 insertions(+) create mode 100644 protocol/build.gradle.kts create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Request.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestDecoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestEncoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Response.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/XorDecoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/XorEncoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/js5/XorExtensions.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestDecoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestEncoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/js5/XorDecoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/js5/XorEncoderTest.kt create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1019-prefetch.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1019-urgent.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1019.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1020-prefetch.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1020-urgent.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1020.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1530-prefetch.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1530-urgent.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1530.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1531-prefetch.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1531-urgent.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/1531.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/508-prefetch.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/508-urgent.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/508.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/509-prefetch.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/509-urgent.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/509.dat create mode 100644 protocol/src/test/resources/org/openrs2/protocol/js5/invalid-block-trailer.dat diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts new file mode 100644 index 00000000..f4911c10 --- /dev/null +++ b/protocol/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `maven-publish` + kotlin("jvm") +} + +dependencies { + api("io.netty:netty-codec:${Versions.netty}") + + implementation(project(":buffer")) + + testImplementation(project(":buffer")) +} + +publishing { + publications.create("maven") { + from(components["java"]) + + pom { + packaging = "jar" + name.set("OpenRS2 Protocol") + description.set( + """ + An implementation of the RuneScape protocol. + """.trimIndent() + ) + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Request.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Request.kt new file mode 100644 index 00000000..99ac827e --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Request.kt @@ -0,0 +1,15 @@ +package org.openrs2.protocol.js5 + +public sealed class Js5Request { + public data class Group( + public val prefetch: Boolean, + public val archive: Int, + public val group: Int + ) : Js5Request() + + public object LoggedIn : Js5Request() + public object LoggedOut : Js5Request() + public data class Rekey(public val key: Int) : Js5Request() + public object Connected : Js5Request() + public object Disconnect : Js5Request() +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestDecoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestDecoder.kt new file mode 100644 index 00000000..13e5bfdb --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestDecoder.kt @@ -0,0 +1,35 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageDecoder +import io.netty.handler.codec.DecoderException + +public class Js5RequestDecoder : ByteToMessageDecoder() { + override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { + if (input.readableBytes() < 4) { + return + } + + val opcode = input.readUnsignedByte().toInt() + + if (opcode == 0 || opcode == 1) { + val archive = input.readUnsignedByte().toInt() + val group = input.readUnsignedShort() + out += Js5Request.Group(opcode == 0, archive, group) + } else if (opcode == 4) { + val key = input.readUnsignedByte().toInt() + input.skipBytes(2) + out += Js5Request.Rekey(key) + } else { + input.skipBytes(3) + out += when (opcode) { + 2 -> Js5Request.LoggedIn + 3 -> Js5Request.LoggedOut + 6 -> Js5Request.Connected + 7 -> Js5Request.Disconnect + else -> throw DecoderException("Unknown JS5 opcode: $opcode") + } + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestEncoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestEncoder.kt new file mode 100644 index 00000000..71f85e71 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5RequestEncoder.kt @@ -0,0 +1,44 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToByteEncoder + +@ChannelHandler.Sharable +public class Js5RequestEncoder : MessageToByteEncoder(Js5Request::class.java) { + override fun encode(ctx: ChannelHandlerContext, msg: Js5Request, out: ByteBuf) { + when (msg) { + is Js5Request.Group -> { + out.writeByte(if (msg.prefetch) 0 else 1) + out.writeByte(msg.archive) + out.writeShort(msg.group) + } + is Js5Request.Rekey -> { + out.writeByte(4) + out.writeByte(msg.key) + out.writeZero(2) + } + is Js5Request.LoggedIn -> encodeSimple(out, 2) + is Js5Request.LoggedOut -> encodeSimple(out, 3) + is Js5Request.Connected -> { + out.writeByte(6) + out.writeMedium(3) + } + is Js5Request.Disconnect -> encodeSimple(out, 7) + } + } + + private fun encodeSimple(out: ByteBuf, opcode: Int) { + out.writeByte(opcode) + out.writeZero(3) + } + + override fun allocateBuffer(ctx: ChannelHandlerContext, msg: Js5Request, preferDirect: Boolean): ByteBuf { + return if (preferDirect) { + ctx.alloc().ioBuffer(4, 4) + } else { + ctx.alloc().heapBuffer(4, 4) + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Response.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Response.kt new file mode 100644 index 00000000..a536cec6 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5Response.kt @@ -0,0 +1,43 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.util.ReferenceCounted + +public data class Js5Response( + public val prefetch: Boolean, + public val archive: Int, + public val group: Int, + public val data: ByteBuf +) : ReferenceCounted { + override fun refCnt(): Int { + return data.refCnt() + } + + override fun retain(): Js5Response { + data.retain() + return this + } + + override fun retain(increment: Int): Js5Response { + data.retain(increment) + return this + } + + override fun touch(): Js5Response { + data.touch() + return this + } + + override fun touch(hint: Any?): Js5Response { + data.touch(hint) + return this + } + + override fun release(): Boolean { + return data.release() + } + + override fun release(decrement: Int): Boolean { + return data.release(decrement) + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoder.kt new file mode 100644 index 00000000..5e4fe156 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoder.kt @@ -0,0 +1,97 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageDecoder +import io.netty.handler.codec.DecoderException +import kotlin.math.min + +public class Js5ResponseDecoder : ByteToMessageDecoder() { + private enum class State { + READ_HEADER, + READ_DATA + } + + private var state = State.READ_HEADER + private var prefetch: Boolean = false + private var archive: Int = 0 + private var group: Int = 0 + private var type: Int = 0 + private var data: ByteBuf = Unpooled.EMPTY_BUFFER + + override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { + if (state == State.READ_HEADER) { + if (input.readableBytes() < 8) { + return + } + + archive = input.readUnsignedByte().toInt() + group = input.readUnsignedShort() + type = input.readUnsignedByte().toInt() + + if (type and 0x80 != 0) { + prefetch = true + type = type and 0x80.inv() + } else { + prefetch = false + } + + val len = input.readInt() + if (len < 0) { + throw DecoderException("Length is negative: $len") + } + + val totalLen = if (type == 0) { + len + 5 + } else { + len + 9 + } + + if (totalLen < 0) { + throw DecoderException("Total length exceeds maximum ByteBuf size") + } + + // TODO(gpe): release data here? + data = ctx.alloc().buffer(totalLen, totalLen) + data.writeByte(type) + data.writeInt(len) + + state = State.READ_DATA + } + + if (state == State.READ_DATA) { + while (data.isWritable) { + val blockLen = min(511 - ((data.readableBytes() + 3) % 511), data.writableBytes()) + val last = data.writableBytes() <= blockLen + + val blockLenIncludingTrailer = if (last) { + blockLen + } else { + blockLen + 1 + } + + if (input.readableBytes() < blockLenIncludingTrailer) { + return + } + + data.writeBytes(input, blockLen) + + if (!last && input.readUnsignedByte().toInt() != 0xFF) { + throw DecoderException("Invalid block trailer") + } + } + + out += Js5Response(prefetch, archive, group, data) + + data = Unpooled.EMPTY_BUFFER + + state = State.READ_HEADER + } + } + + override fun handlerRemoved0(ctx: ChannelHandlerContext?) { + data.release() + data = Unpooled.EMPTY_BUFFER + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoder.kt new file mode 100644 index 00000000..ccd6882e --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoder.kt @@ -0,0 +1,45 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.EncoderException +import io.netty.handler.codec.MessageToByteEncoder +import kotlin.math.min + +@ChannelHandler.Sharable +public class Js5ResponseEncoder : MessageToByteEncoder(Js5Response::class.java) { + override fun encode(ctx: ChannelHandlerContext, msg: Js5Response, out: ByteBuf) { + out.writeByte(msg.archive) + out.writeShort(msg.group) + + if (!msg.data.isReadable) { + // TOOD(gpe): check if the entire container is well-formed? + throw EncoderException("Missing compression byte") + } + + var compression = msg.data.readUnsignedByte().toInt() + if (msg.prefetch) { + compression = compression xor 0x80 + } + out.writeByte(compression) + + out.writeBytes(msg.data, min(msg.data.readableBytes(), 507)) + + while (msg.data.isReadable) { + out.writeByte(0xFF) + out.writeBytes(msg.data, min(msg.data.readableBytes(), 511)) + } + } + + override fun allocateBuffer(ctx: ChannelHandlerContext, msg: Js5Response, preferDirect: Boolean): ByteBuf { + val dataLen = msg.data.readableBytes() + val len = 3 + dataLen + (3 + dataLen) / 511 + + return if (preferDirect) { + ctx.alloc().ioBuffer(len, len) + } else { + ctx.alloc().heapBuffer(len, len) + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorDecoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorDecoder.kt new file mode 100644 index 00000000..7a9a4584 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorDecoder.kt @@ -0,0 +1,13 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToMessageDecoder + +public class XorDecoder : MessageToMessageDecoder(ByteBuf::class.java) { + public var key: Int = 0 + + override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { + out += msg.xor(key) + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorEncoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorEncoder.kt new file mode 100644 index 00000000..a67f05f8 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorEncoder.kt @@ -0,0 +1,13 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToMessageEncoder + +public class XorEncoder : MessageToMessageEncoder(ByteBuf::class.java) { + public var key: Int = 0 + + override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { + out += msg.xor(key) + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorExtensions.kt b/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorExtensions.kt new file mode 100644 index 00000000..82f561ce --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/js5/XorExtensions.kt @@ -0,0 +1,35 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf + +internal fun ByteBuf.xor(key: Int): ByteBuf { + if (key == 0) { + return retain() + } + + val buf = if (refCnt() == 1) { + retain() + } else { + copy() + } + + if (buf.hasArray()) { + val array = buf.array() + + val off = buf.arrayOffset() + buf.readerIndex() + val len = buf.readableBytes() + + for (i in off until off + len) { + array[i] = (array[i].toInt() xor key).toByte() + } + } else { + val off = buf.readerIndex() + val len = buf.readableBytes() + + for (i in off until off + len) { + buf.setByte(i, buf.getByte(i).toInt() xor key) + } + } + + return buf +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestDecoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestDecoderTest.kt new file mode 100644 index 00000000..4cf41107 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestDecoderTest.kt @@ -0,0 +1,45 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.DecoderException +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals + +object Js5RequestDecoderTest { + @Test + fun testDecode() { + testDecode(byteArrayOf(0, 2, 0, 3), Js5Request.Group(true, 2, 3)) + testDecode(byteArrayOf(1, 2, 0, 3), Js5Request.Group(false, 2, 3)) + testDecode(byteArrayOf(4, 0x55, 0, 0), Js5Request.Rekey(0x55)) + testDecode(byteArrayOf(2, 0, 0, 0), Js5Request.LoggedIn) + testDecode(byteArrayOf(3, 0, 0, 0), Js5Request.LoggedOut) + testDecode(byteArrayOf(6, 0, 0, 3), Js5Request.Connected) + testDecode(byteArrayOf(7, 0, 0, 0), Js5Request.Disconnect) + } + + @Test + fun testFragmented() { + val channel = EmbeddedChannel(Js5RequestDecoder()) + channel.writeInbound(Unpooled.wrappedBuffer(byteArrayOf(0, 2))) + channel.writeInbound(Unpooled.wrappedBuffer(byteArrayOf(0, 3))) + assertEquals(Js5Request.Group(true, 2, 3), channel.readInbound()) + } + + @Test + fun testUnknownOpcode() { + val channel = EmbeddedChannel(Js5RequestDecoder()) + + assertThrows { + channel.writeInbound(Unpooled.wrappedBuffer(byteArrayOf(8, 0, 0, 0))) + } + } + + private fun testDecode(bytes: ByteArray, expected: Js5Request) { + val channel = EmbeddedChannel(Js5RequestDecoder()) + + channel.writeInbound(Unpooled.wrappedBuffer(bytes)) + assertEquals(expected, channel.readInbound()) + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestEncoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestEncoderTest.kt new file mode 100644 index 00000000..881731c2 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5RequestEncoderTest.kt @@ -0,0 +1,32 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import org.openrs2.buffer.use +import kotlin.test.Test +import kotlin.test.assertEquals + +object Js5RequestEncoderTest { + @Test + fun testEncode() { + testEncode(Js5Request.Group(true, 2, 3), byteArrayOf(0, 2, 0, 3)) + testEncode(Js5Request.Group(false, 2, 3), byteArrayOf(1, 2, 0, 3)) + testEncode(Js5Request.Rekey(0x55), byteArrayOf(4, 0x55, 0, 0)) + testEncode(Js5Request.LoggedIn, byteArrayOf(2, 0, 0, 0)) + testEncode(Js5Request.LoggedOut, byteArrayOf(3, 0, 0, 0)) + testEncode(Js5Request.Connected, byteArrayOf(6, 0, 0, 3)) + testEncode(Js5Request.Disconnect, byteArrayOf(7, 0, 0, 0)) + } + + private fun testEncode(request: Js5Request, expected: ByteArray) { + val channel = EmbeddedChannel(Js5RequestEncoder()) + channel.writeOutbound(request) + + channel.readOutbound().use { actual -> + Unpooled.wrappedBuffer(expected).use { expected -> + assertEquals(expected, actual) + } + } + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoderTest.kt new file mode 100644 index 00000000..76f6aadb --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseDecoderTest.kt @@ -0,0 +1,138 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.DecoderException +import org.junit.jupiter.api.assertThrows +import org.openrs2.buffer.use +import kotlin.test.Test +import kotlin.test.assertEquals + +object Js5ResponseDecoderTest { + @Test + fun testDecode() { + testDecode("508.dat", "508-prefetch.dat", true) + testDecode("508.dat", "508-urgent.dat", false) + + testDecode("509.dat", "509-prefetch.dat", true) + testDecode("509.dat", "509-urgent.dat", false) + + testDecode("1019.dat", "1019-prefetch.dat", true) + testDecode("1019.dat", "1019-urgent.dat", false) + + testDecode("1020.dat", "1020-prefetch.dat", true) + testDecode("1020.dat", "1020-urgent.dat", false) + + testDecode("1530.dat", "1530-prefetch.dat", true) + testDecode("1530.dat", "1530-urgent.dat", false) + + testDecode("1531.dat", "1531-prefetch.dat", true) + testDecode("1531.dat", "1531-urgent.dat", false) + } + + @Test + fun testDecodeFragmented() { + val channel = EmbeddedChannel(Js5ResponseDecoder()) + + channel.writeInbound(Unpooled.wrappedBuffer(byteArrayOf(2, 0, 3, 0, 0, 0, 0))) + channel.writeInbound( + Unpooled.wrappedBuffer( + byteArrayOf( + 7, + 'O'.toByte(), + 'p'.toByte(), + 'e'.toByte(), + 'n'.toByte() + ) + ) + ) + channel.writeInbound(Unpooled.wrappedBuffer(byteArrayOf('R'.toByte(), 'S'.toByte(), '2'.toByte()))) + + Unpooled.buffer().use { buf -> + buf.writeByte(0) + buf.writeInt(7) + buf.writeCharSequence("OpenRS2", Charsets.UTF_8) + + val expected = Js5Response(false, 2, 3, buf) + + channel.readInbound().use { actual -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testDecodeNegativeLength() { + val channel = EmbeddedChannel(Js5ResponseDecoder()) + + assertThrows { + channel.writeInbound( + Unpooled.wrappedBuffer( + byteArrayOf( + 2, 0, 3, 0, 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte() + ) + ) + ) + } + } + + @Test + fun testDecodeOverflowUncompressed() { + val channel = EmbeddedChannel(Js5ResponseDecoder()) + + assertThrows { + channel.writeInbound( + Unpooled.wrappedBuffer( + byteArrayOf( + 2, 0, 3, 0, 0x7F, 0xFF.toByte(), 0xFF.toByte(), 0xFB.toByte() + ) + ) + ) + } + } + + @Test + fun testDecodeOverflowCompressed() { + val channel = EmbeddedChannel(Js5ResponseDecoder()) + + assertThrows { + channel.writeInbound( + Unpooled.wrappedBuffer( + byteArrayOf( + 2, 0, 3, 1, 0x7F, 0xFF.toByte(), 0xFF.toByte(), 0xF7.toByte() + ) + ) + ) + } + } + + @Test + fun testDecodeInvalidBlockTrailer() { + val channel = EmbeddedChannel(Js5ResponseDecoder()) + + assertThrows { + channel.writeInbound(read("invalid-block-trailer.dat")) + } + } + + private fun testDecode(container: String, encoded: String, prefetch: Boolean) { + val channel = EmbeddedChannel(Js5ResponseDecoder()) + + channel.writeInbound(read(encoded)) + + read(container).use { data -> + val expected = Js5Response(prefetch, 2, 3, data) + + channel.readInbound().use { actual -> + assertEquals(expected, actual) + } + } + } + + private fun read(name: String): ByteBuf { + Js5ResponseDecoderTest::class.java.getResourceAsStream(name).use { input -> + return Unpooled.wrappedBuffer(input.readAllBytes()) + } + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoderTest.kt new file mode 100644 index 00000000..d93da0c6 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/js5/Js5ResponseEncoderTest.kt @@ -0,0 +1,60 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.EncoderException +import org.junit.jupiter.api.assertThrows +import org.openrs2.buffer.use +import kotlin.test.Test +import kotlin.test.assertEquals + +object Js5ResponseEncoderTest { + @Test + fun testEncode() { + testEncode("508.dat", "508-prefetch.dat", true) + testEncode("508.dat", "508-urgent.dat", false) + + testEncode("509.dat", "509-prefetch.dat", true) + testEncode("509.dat", "509-urgent.dat", false) + + testEncode("1019.dat", "1019-prefetch.dat", true) + testEncode("1019.dat", "1019-urgent.dat", false) + + testEncode("1020.dat", "1020-prefetch.dat", true) + testEncode("1020.dat", "1020-urgent.dat", false) + + testEncode("1530.dat", "1530-prefetch.dat", true) + testEncode("1530.dat", "1530-urgent.dat", false) + + testEncode("1531.dat", "1531-prefetch.dat", true) + testEncode("1531.dat", "1531-urgent.dat", false) + } + + @Test + fun testEncodeEmpty() { + val channel = EmbeddedChannel(Js5ResponseEncoder()) + + assertThrows { + channel.writeOutbound(Js5Response(true, 2, 3, Unpooled.EMPTY_BUFFER)) + } + } + + private fun testEncode(container: String, encoded: String, prefetch: Boolean) { + val channel = EmbeddedChannel(Js5ResponseEncoder()) + + read(container).use { buf -> + channel.writeOutbound(Js5Response(prefetch, 2, 3, buf.retain())) + } + + read(encoded).use { expected -> + assertEquals(expected, channel.readOutbound()) + } + } + + private fun read(name: String): ByteBuf { + Js5ResponseEncoderTest::class.java.getResourceAsStream(name).use { input -> + return Unpooled.wrappedBuffer(input.readAllBytes()) + } + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/js5/XorDecoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/js5/XorDecoderTest.kt new file mode 100644 index 00000000..ffd51e3c --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/js5/XorDecoderTest.kt @@ -0,0 +1,40 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import org.openrs2.buffer.use +import kotlin.test.Test +import kotlin.test.assertEquals + +object XorDecoderTest { + @Test + fun testDecode() { + testDecode(0, "OpenRS2", false) + testDecode(0, "OpenRS2", true) + testDecode(32, "oPENrs\u0012", false) + testDecode(32, "oPENrs\u0012", true) + } + + private fun testDecode(key: Int, expected: String, direct: Boolean) { + val decoder = XorDecoder() + decoder.key = key + + val channel = EmbeddedChannel(decoder) + if (direct) { + PooledByteBufAllocator.DEFAULT.ioBuffer().use { buf -> + buf.writeBytes("OpenRS2".toByteArray()) + channel.writeInbound(buf.retain()) + } + } else { + channel.writeInbound(Unpooled.wrappedBuffer("OpenRS2".toByteArray())) + } + + channel.readInbound().use { actual -> + Unpooled.wrappedBuffer(expected.toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/js5/XorEncoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/js5/XorEncoderTest.kt new file mode 100644 index 00000000..04d77bae --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/js5/XorEncoderTest.kt @@ -0,0 +1,40 @@ +package org.openrs2.protocol.js5 + +import io.netty.buffer.ByteBuf +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import org.openrs2.buffer.use +import kotlin.test.Test +import kotlin.test.assertEquals + +object XorEncoderTest { + @Test + fun testEncode() { + testEncode(0, "OpenRS2", false) + testEncode(0, "OpenRS2", true) + testEncode(32, "oPENrs\u0012", false) + testEncode(32, "oPENrs\u0012", true) + } + + private fun testEncode(key: Int, expected: String, direct: Boolean) { + val encoder = XorEncoder() + encoder.key = key + + val channel = EmbeddedChannel(encoder) + if (direct) { + PooledByteBufAllocator.DEFAULT.ioBuffer().use { buf -> + buf.writeBytes("OpenRS2".toByteArray()) + channel.writeOutbound(buf.retain()) + } + } else { + channel.writeOutbound(Unpooled.wrappedBuffer("OpenRS2".toByteArray())) + } + + channel.readOutbound().use { actual -> + Unpooled.wrappedBuffer(expected.toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } +} diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1019-prefetch.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1019-prefetch.dat new file mode 100644 index 0000000000000000000000000000000000000000..5d2ee5cede5cc0d6023d62b63d9a8d71d9d1dc22 GIT binary patch literal 1023 kcmZQ#U~XVwVE*P`keU}1Y&1%ZtRVP5vb;O$TeK_=074&M)Bpeg literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1019-urgent.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1019-urgent.dat new file mode 100644 index 0000000000000000000000000000000000000000..5efce6780fbe1e8627d9db1af6d94a86a10bb7c4 GIT binary patch literal 1023 kcmZQ#U}j)oVE*P`keU}1Y&1%ZtRVP5vb;O$TeK_=07L;_Q~&?~ literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1019.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1019.dat new file mode 100644 index 0000000000000000000000000000000000000000..a8a624b63cafd58f83dea7e9eab8a297d7bb7a9e GIT binary patch literal 1019 dcmZQzU|{~{Uyzy?6l^p~jRwJJDi|>t0RV%HUQhr4 literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1020-prefetch.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1020-prefetch.dat new file mode 100644 index 0000000000000000000000000000000000000000..7afdc6b3791092d129cac6e9a90e715ad437ca8e GIT binary patch literal 1025 mcmZQ#U~XVwVE*o3keU}1Y&1%ZtRVP5vb;O$TeK|x-v|H*0AU6I literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1020-urgent.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1020-urgent.dat new file mode 100644 index 0000000000000000000000000000000000000000..57aa6a813ce790620a10b8bd350b08b87dee97ba GIT binary patch literal 1025 mcmZQ#U}j)oVE*o3keU}1Y&1%ZtRVP5vb;O$TeK|x-v|H*;$VgV literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1020.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1020.dat new file mode 100644 index 0000000000000000000000000000000000000000..81dfca87d098d67963ced40e92b5655b504fd44f GIT binary patch literal 1020 dcmZQzU||04Uyzy?6l^p~jRwJJDi}E#0RZL(UV{Jt literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1530-prefetch.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1530-prefetch.dat new file mode 100644 index 0000000000000000000000000000000000000000..eee39ab81733408a963fab6d768de48b7a45fcbc GIT binary patch literal 1535 qcmZQ#U~XVwVEyV}keU}1Y&1%ZtRVP5vb;O$TeK`b8vi4^&;tP1SrF6! literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1530-urgent.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1530-urgent.dat new file mode 100644 index 0000000000000000000000000000000000000000..22c4d38a7a5b3953ddd4f471a73d93337346dc8c GIT binary patch literal 1535 qcmZQ#U}j)oVEyV}keU}1Y&1%ZtRVP5vb;O$TeK`b8vi4^&;tP3ED%%x literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1530.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1530.dat new file mode 100644 index 0000000000000000000000000000000000000000..af31cb743e7dec06d454af45d5117d5795402af5 GIT binary patch literal 1530 icmZQzU|{{~Uyzy?6l^p~jRwJJDj3ZOqh-NJC=LO%DGpHp literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1531-prefetch.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1531-prefetch.dat new file mode 100644 index 0000000000000000000000000000000000000000..fe8e66b4f1f98e04391515d3343d40cf37812ee5 GIT binary patch literal 1537 scmZQ#U~XVwVEyJ_keU}1Y&1%ZtRVP5vb;O$TeK`b8vi4^(ED!$00BA@1^@s6 literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1531-urgent.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1531-urgent.dat new file mode 100644 index 0000000000000000000000000000000000000000..09993f120bbd9c3c09722b45dfaca9416fa6610b GIT binary patch literal 1537 scmZQ#U}j)oVEyJ_keU}1Y&1%ZtRVP5vb;O$TeK`b8vi4^(ED!$00IpWh5!Hn literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/1531.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/1531.dat new file mode 100644 index 0000000000000000000000000000000000000000..04ad5cff2048e2a70989582f0c93679d5ec083a2 GIT binary patch literal 1531 icmZQzU|{{`Uyzy?6l^p~jRwJJDj3ZOqh-N}C=LP0qYi`s literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/508-prefetch.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/508-prefetch.dat new file mode 100644 index 0000000000000000000000000000000000000000..baed755cc63d48ccc5709754dba2dd7b17f29d1b GIT binary patch literal 511 bcmZQ#U~XVwVEpc1keU}1Y&1%ZtRMgYrIfDJ literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/508-urgent.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/508-urgent.dat new file mode 100644 index 0000000000000000000000000000000000000000..040ac8bd5a9334edd2b6734979c820db4d4d5fea GIT binary patch literal 511 bcmZQ#U}j)oVEpc1keU}1Y&1%ZtRMgYr-iOm literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/508.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/508.dat new file mode 100644 index 0000000000000000000000000000000000000000..6500e76ca0f033faf222a15d7c0b5693d00e0c48 GIT binary patch literal 508 YcmZQzU|{_2Uyzy?6l^p~jjSL50G*AlPXGV_ literal 0 HcmV?d00001 diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/509-prefetch.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/509-prefetch.dat new file mode 100644 index 0000000000000000000000000000000000000000..8b7b5f6e7dcdb3121deeef51d62bbb64444f55d4 GIT binary patch literal 513 dcmZQ#U~XVwVEo}