diff --git a/runtime-lite/src/main/kotlin/Marshaller.kt b/runtime-lite/src/main/kotlin/Marshaller.kt index cfe7750..6e04486 100644 --- a/runtime-lite/src/main/kotlin/Marshaller.kt +++ b/runtime-lite/src/main/kotlin/Marshaller.kt @@ -1,71 +1,69 @@ package jp.co.panpanini -import com.google.protobuf.CodedOutputStream - -class Marshaller(private val stream: CodedOutputStream, private val bytes: ByteArray? = null) { +class Marshaller internal constructor(private val writer: Writer) { companion object { - fun allocate(size: Int) = ByteArray(size).let { Marshaller(CodedOutputStream.newInstance(it), it) } + fun allocate(size: Int) = Marshaller(Writer.allocate(size)) } - fun writeTag(tag: Int) = this.apply { stream.writeInt32NoTag(tag) } + fun writeTag(tag: Int) = this.apply { writer.writeTag(tag) } - fun writeTag(fieldNum: Int, wireType: Int) = this.apply { stream.writeInt32NoTag((fieldNum shl 3) or wireType) } + fun writeTag(fieldNum: Int, wireType: Int) = this.apply { writer.writeTag((fieldNum shl 3) or wireType) } fun writeDouble(value: Double) { - stream.writeDoubleNoTag(value) + writer.writeDouble(value) } fun writeFloat(value: Float) { - stream.writeFloatNoTag(value) + writer.writeFloat(value) } fun writeInt32(value: Int) { - stream.writeInt32NoTag(value) + writer.writeInt32(value) } fun writeInt64(value: Long) { - stream.writeInt64NoTag(value) + writer.writeInt64(value) } fun writeUInt32(value: Int) { - stream.writeUInt32NoTag(value) + writer.writeUInt32(value) } fun writeUInt64(value: Long) { - stream.writeUInt64NoTag(value) + writer.writeUInt64(value) } fun writeSInt32(value: Int) { - stream.writeSInt32NoTag(value) + writer.writeSInt32(value) } fun writeSInt64(value: Long) { - stream.writeSInt64NoTag(value) + writer.writeSInt64(value) } fun writeFixed32(value: Int) { - stream.writeFixed32NoTag(value) + writer.writeFixed32(value) } fun writeFixed64(value: Long) { - stream.writeFixed64NoTag(value) + writer.writeFixed64(value) } fun writeSFixed32(value: Int) { - stream.writeSFixed32NoTag(value) + writer.writeSFixed32(value) } fun writeSFixed64(value: Long) { - stream.writeSFixed64NoTag(value) + writer.writeSFixed64(value) } fun writeBool(value: Boolean) { - stream.writeBoolNoTag(value) + writer.writeBool(value) } fun writeString(value: String) { - stream.writeStringNoTag(value) + writer.writeString(value) } fun writeBytes(value: ByteArr) { @@ -73,7 +71,7 @@ class Marshaller(private val stream: CodedOutputStream, private val bytes: ByteA } fun writeBytes(value: ByteArray) { - stream.writeByteArrayNoTag(value) + writer.writeBytes(value) } fun writeUnknownFields(fields: Map) { @@ -100,7 +98,7 @@ class Marshaller(private val stream: CodedOutputStream, private val bytes: ByteA writeInt32(value.value) } - fun complete() = bytes + fun complete() = writer.complete() fun > writeMap( tag: Int, diff --git a/runtime-lite/src/main/kotlin/Utf8.kt b/runtime-lite/src/main/kotlin/Utf8.kt new file mode 100644 index 0000000..cf3507e --- /dev/null +++ b/runtime-lite/src/main/kotlin/Utf8.kt @@ -0,0 +1,167 @@ +internal object Utf8 { + const val MAX_BYTES_PER_CHAR = 3 + + internal class UnpairedSurrogateException(index: Int, length: Int) : + IllegalArgumentException("Unpaired surrogate at index $index of $length") + + fun encode(input: CharSequence, out: ByteArray, offset: Int, length: Int): Int { + val utf16Length = input.length + var j = offset + var i = 0 + val limit = offset + length + // Designed to take advantage of + // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination + run { + var c: Char? = null + while (i < utf16Length && i + j < limit && input[i].also { c = it }.toInt() < 0x80) { + out[j + i] = c!!.toByte() //TODO: can we do this without !! + i++ + } + } + if (i == utf16Length) { + return j + utf16Length + } + j += i + var c: Char + while (i < utf16Length) { + c = input[i] + if (c.toInt() < 0x80 && j < limit) { + out[j++] = c.toByte() + } else if (c.toInt() < 0x800 && j <= limit - 2) { // 11 bits, two UTF-8 bytes + out[j++] = (0xF shl 6 or (c.toInt() ushr 6)).toByte() + out[j++] = (0x80 or (0x3F and c.toInt())).toByte() + } else if ((c < Char.MIN_SURROGATE || Char.MAX_SURROGATE < c) && j <= limit - 3) { + // Maximum single-char code point is 0xFFFF, 16 bits, three UTF-8 bytes + out[j++] = (0xF shl 5 or (c.toInt() ushr 12)).toByte() + out[j++] = (0x80 or (0x3F and (c.toInt() ushr 6))).toByte() + out[j++] = (0x80 or (0x3F and c.toInt())).toByte() + } else if (j <= limit - 4) { + // Minimum code point represented by a surrogate pair is 0x10000, 17 bits, + // four UTF-8 bytes + var low: Char? = null + if (i + 1 == input.length + || !Character.isSurrogatePair(c, input[++i].also { low = it })) { + throw UnpairedSurrogateException(i - 1, utf16Length) + } + val codePoint: Int = Character.toCodePoint(c, low!!) //TODO: can I do this without !! + out[j++] = (0xF shl 4 or (codePoint ushr 18)).toByte() + out[j++] = (0x80 or (0x3F and (codePoint ushr 12))).toByte() + out[j++] = (0x80 or (0x3F and (codePoint ushr 6))).toByte() + out[j++] = (0x80 or (0x3F and codePoint)).toByte() + } else { + // If we are surrogates and we're not a surrogate pair, always throw an + // UnpairedSurrogateException instead of an ArrayOutOfBoundsException. + if (Char.MIN_SURROGATE <= c && c <= Char.MAX_SURROGATE + && (i + 1 == input.length + || !Character.isSurrogatePair(c, input[i + 1]))) { + throw UnpairedSurrogateException(i, utf16Length) + } + throw IndexOutOfBoundsException("Failed writing $c at index $j") + } + i++ + } + return j + } + + /** + * Returns the number of bytes in the UTF-8-encoded form of `sequence`. For a string, + * this method is equivalent to `string.getBytes(UTF_8).length`, but is more efficient in + * both time and space. + * + * @throws IllegalArgumentException if `sequence` contains ill-formed UTF-16 (unpaired + * surrogates) + */ + fun encodedLength(sequence: CharSequence): Int { + // Warning to maintainers: this implementation is highly optimized. + val utf16Length = sequence.length + var utf8Length = utf16Length + var i = 0 + + // This loop optimizes for pure ASCII. + while (i < utf16Length && sequence[i].toInt() < 0x80) { + i++ + } + + // This loop optimizes for chars less than 0x800. + while (i < utf16Length) { + val c = sequence[i] + if (c.toInt() < 0x800) { + utf8Length += 0x7f - c.toInt() ushr 31 // branch free! + } else { + utf8Length += encodedLengthGeneral(sequence, i) + break + } + i++ + } + if (utf8Length < utf16Length) { + // Necessary and sufficient condition for overflow because of maximum 3x expansion + throw IllegalArgumentException("UTF-8 length does not fit in int: " + + (utf8Length + (1L shl 32))) + } + return utf8Length + } + + private fun encodedLengthGeneral(sequence: CharSequence, start: Int): Int { + val utf16Length = sequence.length + var utf8Length = 0 + var i = start + while (i < utf16Length) { + val c = sequence[i] + if (c.toInt() < 0x800) { + utf8Length += 0x7f - c.toInt() ushr 31 // branch free! + } else { + utf8Length += 2 + // jdk7+: if (Character.isSurrogate(c)) { + if (Char.MIN_SURROGATE <= c && c <= Char.MAX_SURROGATE) { + // Check that we have a well-formed surrogate pair. + val cp: Int = Character.codePointAt(sequence, i) + if (cp < Character.MIN_SUPPLEMENTARY_CODE_POINT) { + throw UnpairedSurrogateException(i, utf16Length) + } + i++ + } + } + i++ + } + return utf8Length + } +} + +object Character { + + const val MIN_SUPPLEMENTARY_CODE_POINT = 0x010000 + + fun isSurrogatePair(high: Char, low: Char): Boolean { + return isHighSurrogate(high) && isLowSurrogate(low) + } + + fun isHighSurrogate(ch: Char): Boolean { + return ch >= Char.MIN_HIGH_SURROGATE && ch.toInt() < Char.MAX_HIGH_SURROGATE.toInt() + 1 + } + + fun isLowSurrogate(ch: Char): Boolean { + return ch >= Char.MIN_LOW_SURROGATE && ch.toInt() < Char.MAX_LOW_SURROGATE.toInt() + 1 + } + + fun toCodePoint(high: Char, low: Char): Int { + // Optimized form of: + // return ((high - MIN_HIGH_SURROGATE) << 10) + // + (low - MIN_LOW_SURROGATE) + // + MIN_SUPPLEMENTARY_CODE_POINT; + return (high.toInt() shl 10) + low.toInt() + (MIN_SUPPLEMENTARY_CODE_POINT + - (Char.MIN_HIGH_SURROGATE.toInt() shl 10) + - Char.MIN_LOW_SURROGATE.toInt()) + } + + fun codePointAt(seq: CharSequence, index: Int): Int { + var index = index + val c1 = seq[index] + if (isHighSurrogate(c1) && ++index < seq.length) { + val c2 = seq[index] + if (isLowSurrogate(c2)) { + return toCodePoint(c1, c2) + } + } + return c1.toInt() + } +} \ No newline at end of file diff --git a/runtime-lite/src/main/kotlin/Writer.kt b/runtime-lite/src/main/kotlin/Writer.kt new file mode 100644 index 0000000..a08bec3 --- /dev/null +++ b/runtime-lite/src/main/kotlin/Writer.kt @@ -0,0 +1,167 @@ +package jp.co.panpanini + +import Utf8 + +internal class Writer(private val byteArray: ByteArray, private val utf8: Utf8 = Utf8) { + + private var position = 0 + + fun spaceLeft(): Int = byteArray.size - position + + fun writeTag(value: Int) { + writeUInt32(value) + } + + fun writeInt32(value: Int) { + if (value >= 0) { + writeUInt32(value) + } else { + writeUInt64(value.toLong()) + } + } + + // bytes are written in smallest to largest order + fun writeUInt32(value: Int) { + var value = value + while (true) { + if (value and 0x7F.inv() == 0) { + byteArray[position++] = value.toByte() + return + } else { + byteArray[position++] = (value and 0x7F or 0x80).toByte() + value = value ushr 7 + } + } + } + + // bytes are written in smallest to largest order + fun writeInt64(value: Long) { + writeUInt64(value) + } + + // bytes are written in smallest to largest order + fun writeUInt64(value: Long) { + var value = value + while (true) { + if (value and 0x7FL.inv() == 0L) { + byteArray[position++] = value.toByte() + return + } else { + byteArray[position++] = (value.toInt() and 0x7F or 0x80).toByte() + value = value ushr 7 + } + } + } + + // bytes are written in smallest to largest order + fun writeFixed32(value: Int) { + byteArray[position++] = (value and 0xFF).toByte() + byteArray[position++] = (value shr 8 and 0xFF).toByte() + byteArray[position++] = (value shr 16 and 0xFF).toByte() + byteArray[position++] = (value shr 24 and 0xFF).toByte() + } + + // bytes are written in smallest to largest order + fun writeFixed64(value: Long) { + byteArray[position++] = (value and 0xFF).toByte() + byteArray[position++] = (value shr 8 and 0xFF).toByte() + byteArray[position++] = (value shr 16 and 0xFF).toByte() + byteArray[position++] = (value shr 24 and 0xFF).toByte() + byteArray[position++] = ((value shr 32).toInt() and 0xFF).toByte() + byteArray[position++] = ((value shr 40).toInt() and 0xFF).toByte() + byteArray[position++] = ((value shr 48).toInt() and 0xFF).toByte() + byteArray[position++] = ((value shr 56).toInt() and 0xFF).toByte() + } + + fun writeSInt32(value: Int) { + writeUInt32(value.encodeZigZag32()) + } + + fun writeSInt64(value: Long) { + writeUInt64(value.encodeZigZag64()) + } + + fun writeSFixed32(value: Int) { + writeFixed32(value) + } + + fun writeSFixed64(value: Long) { + writeFixed64(value) + } + + fun writeBool(value: Boolean) { + writeByte((if (value) 1 else 0).toByte()) + } + + fun writeDouble(value: Double) { + writeFixed64(value.toRawBits()) + } + + fun writeFloat(value: Float) { + writeFixed32(value.toRawBits()) + } + + fun writeByte(value: Byte) { + byteArray[position++] = value + } + + fun writeBytes(value: ByteArray) { + value.forEach(::writeByte) + } + + fun writeString(value: String) { + val oldPosition = position + // UTF-8 byte length of the string is at least its UTF-16 code unit length (value.length()), + // and at most 3 times of it. We take advantage of this in both branches below. + val maxLength: Int = value.length * utf8.MAX_BYTES_PER_CHAR + val maxLengthVarIntSize: Int = computeUInt32Size(maxLength) + val minLengthVarIntSize: Int = computeUInt32Size(value.length) + if (minLengthVarIntSize == maxLengthVarIntSize) { + position = oldPosition + minLengthVarIntSize + val newPosition: Int = utf8.encode(value, byteArray, position, spaceLeft()) + // Since this class is stateful and tracks the position, we rewind and store the state, + // prepend the length, then reset it back to the end of the string. + position = oldPosition + val length = newPosition - oldPosition - minLengthVarIntSize + writeUInt32(length) + position = newPosition + } else { + val length: Int = utf8.encodedLength(value) + writeUInt32(length) + position = utf8.encode(value, byteArray, position, spaceLeft()) + } + } + + fun complete(): ByteArray { + return byteArray + } + + companion object { + fun allocate(size: Int) : Writer = Writer(ByteArray(size)) + } +} + +private fun computeUInt32Size(value: Int): Int { + if (value and (0.inv() shl 7) == 0) { + return 1 + } + if (value and (0.inv() shl 14) == 0) { + return 2 + } + if (value and (0.inv() shl 21) == 0) { + return 3 + } + return if (value and (0.inv() shl 28) == 0) { + 4 + } else 5 +} + +fun Int.encodeZigZag32(): Int { + // Note: the right-shift must be arithmetic + return this shl 1 xor (this shr 31) +} + +fun Long.encodeZigZag64(): Long { + // Note: the right-shift must be arithmetic + return this shl 1 xor (this shr 63) +} \ No newline at end of file diff --git a/runtime-lite/src/test/kotlin/MarshallerTest.kt b/runtime-lite/src/test/kotlin/MarshallerTest.kt index 67178c8..673227a 100644 --- a/runtime-lite/src/test/kotlin/MarshallerTest.kt +++ b/runtime-lite/src/test/kotlin/MarshallerTest.kt @@ -1,4 +1,3 @@ -import com.google.protobuf.CodedOutputStream import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.spy import com.nhaarman.mockitokotlin2.verify @@ -6,26 +5,23 @@ import com.nhaarman.mockitokotlin2.whenever import jp.co.panpanini.ByteArr import jp.co.panpanini.Marshaller import jp.co.panpanini.Message -import org.assertj.core.api.Assertions.assertThat +import jp.co.panpanini.Writer import org.junit.Test import org.mockito.ArgumentMatchers.anyInt class MarshallerTest { + private var writer: Writer = mock { } - private var stream: CodedOutputStream = mock { } - - private var bytes: ByteArray? = ByteArray(0) - - private var target: Marshaller = Marshaller(stream, bytes) + private var target: Marshaller = Marshaller(writer) @Test - fun `writeTag should call stream#writeInt32NoTag`() { + fun `writeTag should call stream#writeTag`() { val input = 100 target.writeTag(input) - verify(stream).writeInt32NoTag(input) + verify(writer).writeTag(input) } @Test @@ -37,133 +33,133 @@ class MarshallerTest { target.writeTag(fieldNum, wireType) - verify(stream).writeInt32NoTag(fieldNumSHL3 + wireType) + verify(writer).writeTag(fieldNumSHL3 + wireType) } @Test - fun `writeDouble should call stream#writeDoubleNoTag`() { + fun `writeDouble should call stream#writeDouble`() { val input = 1.0 target.writeDouble(input) - verify(stream).writeDoubleNoTag(input) + verify(writer).writeDouble(input) } @Test - fun `writeFloat should call stream#writeFloatNoTag`() { + fun `writeFloat should call stream#writeFloat`() { val input = 1f target.writeFloat(input) - verify(stream).writeFloatNoTag(input) + verify(writer).writeFloat(input) } @Test - fun `writeInt32 should call stream#writeInt32NoTag`() { + fun `writeInt32 should call stream#writeInt32`() { val input = 1 target.writeInt32(input) - verify(stream).writeInt32NoTag(input) + verify(writer).writeInt32(input) } @Test - fun `writeInt64 should call stream#writeInt64NoTag`() { + fun `writeInt64 should call stream#writeInt64`() { val input = 1L target.writeInt64(input) - verify(stream).writeInt64NoTag(input) + verify(writer).writeInt64(input) } @Test - fun `writeUInt32 should call stream#writeUInt32NoTag`() { + fun `writeUInt32 should call stream#writeUInt32`() { val input = 1 target.writeUInt32(input) - verify(stream).writeUInt32NoTag(input) + verify(writer).writeUInt32(input) } @Test - fun `writeUInt64 should call stream#writeUInt64NoTag`() { + fun `writeUInt64 should call stream#writeUInt64`() { val input = 1L target.writeUInt64(input) - verify(stream).writeUInt64NoTag(input) + verify(writer).writeUInt64(input) } @Test - fun `writeSInt32 should call stream#writeSInt32NoTag`() { + fun `writeSInt32 should call stream#writeSInt32`() { val input = 1 target.writeSInt32(input) - verify(stream).writeSInt32NoTag(input) + verify(writer).writeSInt32(input) } @Test - fun `writeSInt64 should call stream#writeSInt64NoTag`() { + fun `writeSInt64 should call stream#writeSInt64`() { val input = 1L target.writeSInt64(input) - verify(stream).writeSInt64NoTag(input) + verify(writer).writeSInt64(input) } @Test - fun `writeFixed32 should call stream#writeFixed32NoTag`() { + fun `writeFixed32 should call stream#writeFixed32`() { val input = 1 target.writeFixed32(input) - verify(stream).writeFixed32NoTag(input) + verify(writer).writeFixed32(input) } @Test - fun `writeFixed64 should call stream#writeFixed64NoTag`() { + fun `writeFixed64 should call stream#writeFixed64`() { val input = 1L target.writeFixed64(input) - verify(stream).writeFixed64NoTag(input) + verify(writer).writeFixed64(input) } @Test - fun `writeSFixed32 should call stream#writeSFixed32NoTag`() { + fun `writeSFixed32 should call stream#writeSFixed32`() { val input = 1 target.writeSFixed32(input) - verify(stream).writeSFixed32NoTag(input) + verify(writer).writeSFixed32(input) } @Test - fun `writeSFixed64 should call stream#writeSFixed64NoTag`() { + fun `writeSFixed64 should call stream#writeSFixed64`() { val input = 1L target.writeSFixed64(input) - verify(stream).writeSFixed64NoTag(input) + verify(writer).writeSFixed64(input) } @Test - fun `writeBool should call stream#writeBoolNoTag`() { + fun `writeBool should call stream#writeBool`() { val input = true target.writeBool(input) - verify(stream).writeBoolNoTag(input) + verify(writer).writeBool(input) } @Test - fun `writeString should call stream#writeStringNoTag`() { + fun `writeString should call stream#writeString`() { val input = "" target.writeString("") - verify(stream).writeStringNoTag(input) + verify(writer).writeString(input) } @Test @@ -180,12 +176,12 @@ class MarshallerTest { } @Test - fun `writeBytes should call stream#writeByteArrayNoTag`() { + fun `writeBytes should call stream#writeByteArray`() { val input = ByteArray(0) target.writeBytes(input) - verify(stream).writeByteArrayNoTag(input) + verify(writer).writeBytes(input) } @Test @@ -214,11 +210,10 @@ class MarshallerTest { } @Test - fun `complete should return the byte array`() { - - val result = target.complete() + fun `complete should call writer#complete`() { + target.complete() - assertThat(result).isEqualTo(bytes) + verify(writer).complete() } @Test diff --git a/runtime-lite/src/test/kotlin/WriterTest.kt b/runtime-lite/src/test/kotlin/WriterTest.kt new file mode 100644 index 0000000..293c3ae --- /dev/null +++ b/runtime-lite/src/test/kotlin/WriterTest.kt @@ -0,0 +1,297 @@ +package jp.co.panpanini + +import Utf8 +import com.nhaarman.mockitokotlin2.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString + +class WriterTest { + + private val utf8: Utf8 = mock { } + + private lateinit var byteArray: ByteArray + + private lateinit var target: Writer + + + @Before + fun setup() { + byteArray = ByteArray(100) + target = spy(Writer(byteArray, utf8)) + } + + fun setup(size: Int) { + byteArray = ByteArray(size) + target = spy(Writer(byteArray)) + } + + @Test + fun `writeTag should write the UInt32 representation to byteArray`() { + val input = 150 + + target.writeTag(input) + + verify(target).writeUInt32(input) + } + + @Test + fun `writeInt32 should writeUInt32 value when value is greater than 0`() { + val input = 150 + + target.writeInt32(input) + + verify(target).writeUInt32(input) + } + + @Test + fun `writeInt32 should writeUInt64 value when value is less than 0`() { + val input = -150 + target.writeInt32(input) + + verify(target).writeUInt64(input.toLong()) + } + + @Test + fun `writeInt64 should call writeUInt64`() { + val input = 1L + + target.writeInt64(input) + + verify(target).writeUInt64(input) + } + + @Test + fun `writeSInt32 should call writeUInt32 with zigzag encoded value`() { + val input = 1 + + target.writeSInt32(input) + + verify(target).writeUInt32(input.encodeZigZag32()) + } + + @Test + fun `writeSInt64 should call writeUInt64 with zigzag encoded value`() { + val input = 1L + target.writeSInt64(input) + + verify(target).writeUInt64(input.encodeZigZag64()) + } + + @Test + fun `writeSFixed32 should call writeFixed32`() { + val input = 1 + + target.writeSFixed32(input) + + verify(target).writeFixed32(input) + } + + @Test + fun `writeSFixed64 should call writeFixed64`() { + val input = 1L + + target.writeSFixed64(input) + + verify(target).writeFixed64(input) + } + + @Test + fun `writeBool should call writeByte`() { + var input = true + + target.writeBool(input) + + verify(target).writeByte(1.toByte()) + + input = false + + target.writeBool(input) + + verify(target).writeByte(0.toByte()) + } + + @Test + fun `writeDouble should call writeFixed64 with raw bits`() { + val input = 1.0 + + target.writeDouble(input) + + verify(target).writeFixed64(input.toRawBits()) + } + + @Test + fun `writeFloat should call writeFixed32 with raw bits`() { + val input = 1f + + target.writeFloat(input) + + verify(target).writeFixed32(input.toRawBits()) + } + + @Test + fun `writeByte should write the byte to byteArray`() { + setup(1) + val input = 2.toByte() + + target.writeByte(input) + + assertThat(byteArray[0]).isEqualTo(input) + } + + @Test + fun `writeBytes should call writeByte for each value in the byte array`() { + val byte1 = 1.toByte() + val byte2 = 2.toByte() + val input = byteArrayOf(byte1, byte2) + + target.writeBytes(input) + + verify(target).writeByte(byte1) + verify(target).writeByte(byte2) + } + + @Test + fun `complete should return the byteArray`() { + assertThat(target.complete()).isSameAs(byteArray) + } + + @Test + fun `writeUInt32 should write 7bit integer in 1 byte`() { + setup(1) + val input = 127 // largest 7-bit integer + val expected = byteArrayOf(0b01111111.toByte()) + + target.writeUInt32(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeUInt32 should write 32-bit integer in 5 bytes`() { + setup(5) + val input = Integer.MAX_VALUE // largest 32-bit integer + val expected = byteArrayOf( + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b00000111.toByte() + ) + + target.writeUInt32(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeUInt64 should write 7bit long in 1 byte`() { + setup(1) + val input = 127L // largest 7-bit integer + val expected = byteArrayOf(0b01111111.toByte()) + + target.writeUInt64(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeUInt64 should write 32-bit integer in 5 bytes`() { + setup(5) + val input = Integer.MAX_VALUE.toLong() // largest 32-bit integer + val expected = byteArrayOf( + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b00000111.toByte() + ) + + target.writeUInt64(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeUInt64 should write 64-bit integer in 9 bytes`() { + setup(9) + val input = Long.MAX_VALUE // largest 64-bit integer + val expected = byteArrayOf( + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b11111111.toByte(), + 0b01111111.toByte() + ) + + target.writeUInt64(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeFixed32 should write 4 bytes exactly`() { + setup(4) + val input = 2140483647 + val expected = byteArrayOf( + 0b00111111.toByte(), + 0b00110000.toByte(), + 0b10010101.toByte(), + 0b01111111.toByte() + ) + + target.writeFixed32(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeFixed64 should write 8 bytes exactly`() { + setup(8) + val input = 9223302036854775807L + val expected = byteArrayOf( + 0b11111111.toByte(), + 0b10011111.toByte(), + 0b11011101.toByte(), + 0b11011010.toByte(), + 0b01010101.toByte(), + 0b11000000.toByte(), + 0b11111111.toByte(), + 0b01111111.toByte() + ) + + target.writeFixed64(input) + + assertThat(byteArray).isEqualTo(expected) + } + + @Test + fun `writeString should call utf8#encode`() { + val input = "Hello, World!" + val computedLength = 13 + + whenever(utf8.encode(anyString(), any(), anyInt(), anyInt())).thenReturn(computedLength) + + target.writeString(input) + + verify(target).writeUInt32(computedLength - 1) // string length int is 1 byte + verify(utf8).encode(input, byteArray, 1, 99) + } + + @Test + fun `writeString should call utf8#encodedLength for a long string`() { + val input = (0..1000).joinToString { "test$it " } + + target.writeString(input) + + verify(utf8).encodedLength(input) // ask utf8 for the right length + verify(utf8).encode(input, byteArray, 1, 99) + } + +} \ No newline at end of file