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 00000000..5d2ee5ce Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1019-prefetch.dat differ 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 00000000..5efce678 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1019-urgent.dat differ 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 00000000..a8a624b6 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1019.dat differ 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 00000000..7afdc6b3 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1020-prefetch.dat differ 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 00000000..57aa6a81 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1020-urgent.dat differ 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 00000000..81dfca87 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1020.dat differ 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 00000000..eee39ab8 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1530-prefetch.dat differ 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 00000000..22c4d38a Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1530-urgent.dat differ 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 00000000..af31cb74 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1530.dat differ 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 00000000..fe8e66b4 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1531-prefetch.dat differ 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 00000000..09993f12 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1531-urgent.dat differ 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 00000000..04ad5cff Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/1531.dat differ 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 00000000..baed755c Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/508-prefetch.dat differ 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 00000000..040ac8bd Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/508-urgent.dat differ 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 00000000..6500e76c Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/508.dat differ 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 00000000..8b7b5f6e Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/509-prefetch.dat differ diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/509-urgent.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/509-urgent.dat new file mode 100644 index 00000000..d97c7967 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/509-urgent.dat differ diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/509.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/509.dat new file mode 100644 index 00000000..c94b5e7a Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/509.dat differ diff --git a/protocol/src/test/resources/org/openrs2/protocol/js5/invalid-block-trailer.dat b/protocol/src/test/resources/org/openrs2/protocol/js5/invalid-block-trailer.dat new file mode 100644 index 00000000..2450d775 Binary files /dev/null and b/protocol/src/test/resources/org/openrs2/protocol/js5/invalid-block-trailer.dat differ diff --git a/settings.gradle.kts b/settings.gradle.kts index d189456a..1d8c3ac8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ include( "nonfree:signlink", "nonfree:unpack", "nonfree:unpackclass", + "protocol", "util", "yaml" )