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 object 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 or 0x80 } out.writeByte(compression) out.writeBytes(msg.data, min(msg.data.readableBytes(), 508)) 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() /* * The naive code to estimate the length is: * * var len = 3 // for the archive/group header * len += dataLen // for the data itself * * // first 0xFF marker * if (dataLen > 509) { // compression byte plus the first 508 bytes of data * len++ * dataLen -= 509 * * // remaining 0xFF markers * while (dataLen > 511) { * len++ * dataLen -= 511 * } * } * * We can simplify this by combining the if and while loop by adding an * extra 2 bytes to dataLen, which ensures we add the first marker * after 509 bytes (511-2) and then all subsequent markers after 511 * bytes: * * var len = 3 * len += dataLen * * dataLen += 2 * * while (dataLen > 511) { * len++ * dataLen -= 511 * } * * The while loop can be replaced with division: * * var len = 3 * len += dataLen * len += divide(dataLen + 2, 511) * * divide() is almost but not quite standard division. We want * divide(x, 511) to produce 0 if x=511, and only produce 1 if x=512. * Similarly, it should produce 1 if x=1022, and only produce 1 if * x=1023, and so on. * * This can be achieved by implementing divide(x, y) as * ((x + (y - 1)) / y) - 1. So in our case, where y=511, * ((x + 510) / y) - 1. * * Combined with the above, our length calculation becomes: * * var len = 3 * len += dataLen * len += (dataLen + 2 + 510) / 511 - 1 * * which we can simplify to: * * val len = 2 + dataLen + (512 + dataLen) / 511 * * As this is confusing, testAllocateBuffer() has a test to ensure the * length calculation is correct for all dataLens between 1 and 1534 * inclusive, which covers all lengths for 1, 2 and 3 block responses * completely, and one length for a 4 block responses (as a boundary * check). */ val len = 2 + dataLen + (512 + dataLen) / 511 return if (preferDirect) { ctx.alloc().ioBuffer(len, len) } else { ctx.alloc().heapBuffer(len, len) } } }