diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceOutput.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceOutput.kt new file mode 100644 index 000000000..a00696fbd --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceOutput.kt @@ -0,0 +1,292 @@ +package net.mamoe.mirai.qqandroid.io + +import kotlinx.io.charsets.Charset +import kotlinx.io.core.* +import kotlin.experimental.or +import kotlin.reflect.KClass + +@PublishedApi +internal val CharsetGBK = Charset.forName("GBK") +@PublishedApi +internal val CharsetUTF8 = Charset.forName("UTF8") + +inline fun buildJcePacket(stringCharset: Charset = CharsetGBK, block: JceOutput.() -> Unit): ByteReadPacket { + return JceOutput(stringCharset).apply(block).build() +} + +inline fun BytePacketBuilder.writeJcePacket(stringCharset: Charset = CharsetGBK, block: JceOutput.() -> Unit) { + return this.writePacket(buildJcePacket(stringCharset, block)) +} + +fun jceStruct(tag: Int, struct: JceStruct): ByteArray{ + return buildJcePacket { + writeJceStruct(struct, tag) + }.readBytes() +} + +fun jceMap(tag: Int, vararg entries: Pair): ByteArray { + return buildJcePacket { + writeMap(mapOf(*entries), tag) + }.readBytes() +} + +/** + * + * From: com.qq.taf.jce.JceOutputStream + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +@UseExperimental(ExperimentalIoApi::class) +class JceOutput( + private val stringCharset: Charset = CharsetGBK +) { + private val output: BytePacketBuilder = BytePacketBuilder() + + fun build(): ByteReadPacket = output.build() + + fun close() = output.close() + fun flush() = output.flush() + + fun writeByte(v: Byte, tag: Int) { + if (v.toInt() == 0) { + writeHead(ZERO_TYPE, tag) + } else { + writeHead(BYTE, tag) + output.writeByte(v) + } + } + + fun writeDouble(v: Double, tag: Int) { + writeHead(DOUBLE, tag) + output.writeDouble(v) + } + + fun writeFloat(v: Float, tag: Int) { + writeHead(FLOAT, tag) + output.writeFloat(v) + } + + fun writeFully(src: ByteArray, tag: Int) { + writeHead(SIMPLE_LIST, tag) + writeHead(BYTE, 0) + writeInt(src.size, 0) + output.writeFully(src) + } + + fun writeFully(src: DoubleArray, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeDouble(it, 0) + } + } + + fun writeFully(src: FloatArray, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeFloat(it, 0) + } + } + + fun writeFully(src: IntArray, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeInt(it, 0) + } + } + + fun writeFully(src: LongArray, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeLong(it, 0) + } + } + + fun writeFully(src: ShortArray, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeShort(it, 0) + } + } + + fun writeFully(src: BooleanArray, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeBoolean(it, 0) + } + } + + fun writeFully(src: Array, tag: Int) { + writeHead(LIST, tag) + writeInt(src.size, 0) + src.forEach { + writeObject(it, 0) + } + } + + fun writeInt(v: Int, tag: Int) { + if (v in Short.MIN_VALUE..Short.MAX_VALUE) { + writeShort(v.toShort(), tag) + } else { + writeHead(INT, tag) + output.writeInt(v) + } + } + + fun writeLong(v: Long, tag: Int) { + if (v in Int.MIN_VALUE..Int.MAX_VALUE) { + writeInt(v.toInt(), tag) + } else { + writeHead(LONG, tag) + output.writeLong(v) + } + } + + fun writeShort(v: Short, tag: Int) { + if (v in Byte.MIN_VALUE..Byte.MAX_VALUE) { + writeByte(v.toByte(), tag) + } else { + writeHead(SHORT, tag) + output.writeShort(v) + } + } + + fun writeBoolean(v: Boolean, tag: Int) { + this.writeByte(if (v) 1 else 0, tag) + } + + fun writeString(v: String, tag: Int) { + val array = v.toByteArray(stringCharset) + if (array.size > 255) { + writeHead(STRING4, tag) + output.writeInt(array.size) + output.writeFully(array) + } else { + writeHead(STRING1, tag) + output.writeByte(array.size.toByte()) + output.writeFully(array) + } + } + + fun writeMap(map: Map, tag: Int) { + writeHead(MAP, tag) + if (map.isEmpty()) { + writeInt(0, 0) + } else { + writeInt(map.size, 0) + map.forEach { (key, value) -> + writeObject(key, 0) + writeObject(value, 1) + } + } + } + + fun writeCollection(collection: Collection<*>?, tag: Int) { + writeHead(LIST, tag) + if (collection == null || collection.isEmpty()) { + writeInt(0, 0) + } else { + writeInt(collection.size, 0) + collection.forEach { + writeObject(it, 0) + } + } + } + + fun writeJceStruct(v: JceStruct, tag: Int) { + writeHead(STRUCT_BEGIN, tag) + v.writeTo(this) + writeHead(STRUCT_END, 0) + } + + fun writeObject(v: T, tag: Int) { + when (v) { + is Byte -> writeByte(v, tag) + is Short -> writeShort(v, tag) + is Int -> writeInt(v, tag) + is Long -> writeLong(v, tag) + is Float -> writeFloat(v, tag) + is Double -> writeDouble(v, tag) + is JceStruct -> writeJceStruct(v, tag) + is ByteArray -> writeFully(v, tag) + is Collection<*> -> writeCollection(v, tag) + is Boolean -> writeBoolean(v, tag) + is Map<*, *> -> writeMap(v, tag) + is IntArray -> writeFully(v, tag) + is ShortArray -> writeFully(v, tag) + is BooleanArray -> writeFully(v, tag) + is LongArray -> writeFully(v, tag) + is FloatArray -> writeFully(v, tag) + is DoubleArray -> writeFully(v, tag) + is Array<*> -> writeFully(v, tag) + is String -> writeString(v, tag) + +// +// is ByteReadPacket -> ByteArrayPool.useInstance { +// v.readAvailable(it) +// writeFully(it, tag) +// } + else -> error("unsupported type: ${v.getClassName()}") + } + } + + fun write(v: Int, tag: Int) = writeInt(v, tag) + fun write(v: Byte, tag: Int) = writeByte(v, tag) + fun write(v: Short, tag: Int) = writeShort(v, tag) + fun write(v: Long, tag: Int) = writeLong(v, tag) + fun write(v: Float, tag: Int) = writeFloat(v, tag) + fun write(v: Double, tag: Int) = writeDouble(v, tag) + fun write(v: String, tag: Int) = writeString(v, tag) + fun write(v: Boolean, tag: Int) = writeBoolean(v, tag) + fun write(v: Collection<*>, tag: Int) = writeCollection(v, tag) + fun write(v: Map<*, *>, tag: Int) = writeMap(v, tag) + fun write(v: ByteArray, tag: Int) = writeFully(v, tag) + fun write(v: IntArray, tag: Int) = writeFully(v, tag) + fun write(v: BooleanArray, tag: Int) = writeFully(v, tag) + fun write(v: LongArray, tag: Int) = writeFully(v, tag) + fun write(v: ShortArray, tag: Int) = writeFully(v, tag) + fun write(v: Array<*>, tag: Int) = writeFully(v, tag) + fun write(v: FloatArray, tag: Int) = writeFully(v, tag) + fun write(v: DoubleArray, tag: Int) = writeFully(v, tag) + + @PublishedApi + internal companion object { + const val BYTE: Int = 0 + const val DOUBLE: Int = 5 + const val FLOAT: Int = 4 + const val INT: Int = 2 + const val JCE_MAX_STRING_LENGTH = 104857600 + const val LIST: Int = 9 + const val LONG: Int = 3 + const val MAP: Int = 8 + const val SHORT: Int = 1 + const val SIMPLE_LIST: Int = 13 + const val STRING1: Int = 6 + const val STRING4: Int = 7 + const val STRUCT_BEGIN: Int = 10 + const val STRUCT_END: Int = 11 + const val ZERO_TYPE: Int = 12 + + private fun Any?.getClassName(): KClass = if (this == null) Unit::class else this::class + } + + @PublishedApi + internal fun writeHead(type: Int, tag: Int) { + if (tag < 15) { + this.output.writeByte(((tag shl 4) or type).toByte()) + return + } + if (tag < 256) { + this.output.writeByte((type.toByte() or 0xF0.toByte())) + this.output.writeByte(tag.toByte()) + return + } + throw JceEncodeException("tag is too large: $tag") + } +} + +class JceEncodeException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceStruct.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceStruct.kt index adb5a6705..d58d65ca2 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceStruct.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/JceStruct.kt @@ -1,3 +1,5 @@ package net.mamoe.mirai.qqandroid.io -interface JceStruct \ No newline at end of file +interface JceStruct { + fun writeTo(output: JceOutput) = Unit +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/Jce.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/Jce.kt index 9e54407a8..48888da36 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/Jce.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/Jce.kt @@ -13,7 +13,6 @@ import net.mamoe.mirai.qqandroid.io.JceStruct import net.mamoe.mirai.qqandroid.network.protocol.packet.withUse import net.mamoe.mirai.utils.io.readString import net.mamoe.mirai.utils.io.toIoBuffer -import kotlin.reflect.KClass @PublishedApi internal val CharsetGBK = Charset.forName("GBK") @@ -24,7 +23,7 @@ fun ByteArray.loadAs(deserializer: DeserializationStrategy, c: JceCharset return Jce.byCharSet(c).load(deserializer, this) } -fun T.toByteArray(serializer: SerializationStrategy, c: JceCharset = JceCharset.UTF8): ByteArray = Jce.byCharSet(c).dump(serializer, this) +fun T.toByteArray(serializer: SerializationStrategy, c: JceCharset = JceCharset.GBK): ByteArray = Jce.byCharSet(c).dump(serializer, this) enum class JceCharset(val kotlinCharset: Charset) { GBK(Charset.forName("GBK")), @@ -104,7 +103,6 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo @Suppress("UNCHECKED_CAST", "NAME_SHADOWING") override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) = when (serializer.descriptor) { is MapLikeDescriptor -> { - println("hello") val entries = (value as Map<*, *>).entries val serializer = (serializer as MapLikeSerializer) val mapEntrySerial = MapEntrySerializer(serializer.keySerializer, serializer.valueSerializer) @@ -115,24 +113,26 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo } ByteArraySerializer.descriptor -> encodeTaggedByteArray(popTag(), value as ByteArray) is PrimitiveArrayDescriptor -> { - if (value is ByteArray) { - this.encodeTaggedByteArray(popTag(), value) - } else { - serializer.serialize( - ListWriter( - when (value) { - is ShortArray -> value.size - is IntArray -> value.size - is LongArray -> value.size - is FloatArray -> value.size - is DoubleArray -> value.size - is CharArray -> value.size - else -> error("unknown array type: ${value.getClassName()}") - }, popTag(), this - ), - value - ) - } + // if (value is ByteArray) { + // this.encodeTaggedByteArray(popTag(), value) + // } else { + serializer.serialize( + ListWriter( + when (value) { + is ShortArray -> value.size + is IntArray -> value.size + is LongArray -> value.size + is FloatArray -> value.size + is DoubleArray -> value.size + is CharArray -> value.size + is ByteArray -> value.size + is BooleanArray -> value.size + else -> error("unknown array type: ${value.getClassName()}") + }, popTag(), this + ), + value + ) + // } } is ArrayClassDesc -> { serializer.serialize( @@ -260,13 +260,13 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo } @PublishedApi - internal fun writeHead(type: Int, tag: Int) { + internal fun writeHead(type: Byte, tag: Int) { if (tag < 15) { - this.output.write((tag shl 4) or type) + this.output.write((tag shl 4) or type.toInt()) return } if (tag < 256) { - this.output.write(type or 0xF0) + this.output.write(type.toInt() or 0xF0) this.output.write(tag) return } @@ -274,6 +274,45 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo } } + private open inner class JceMapReader( + val size: Int, + input: JceInput + ) : JceDecoder(input) { + override fun decodeCollectionSize(desc: SerialDescriptor): Int { + return size + } + + override fun SerialDescriptor.getTag(index: Int): Int { + // 奇数 0, 即 key; 偶数 1, 即 value + return if (index % 2 == 0) 0 else 1 + } + } + + private open inner class JceListReader( + val size: Int, + input: JceInput + ) : JceDecoder(input) { + override fun decodeCollectionSize(desc: SerialDescriptor): Int { + return size + } + + override fun SerialDescriptor.getTag(index: Int): Int { + return 0 + } + } + + private open inner class JceStructReader( + input: JceInput + ) : JceDecoder(input) { + override fun endStructure(desc: SerialDescriptor) { + input.readHead() // STRUCT_END + } + } + + private open inner class NullReader( + input: JceInput + ) : JceDecoder(input) + private open inner class JceDecoder( internal val input: JceInput ) : TaggedDecoder() { @@ -291,69 +330,112 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo override fun decodeTaggedString(tag: Int): String = input.readString(tag) override fun decodeTaggedBoolean(tag: Int): Boolean = input.readBoolean(tag) + /** + * 在 [KSerializer.serialize] 前 + */ + override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder { + println("beginStructure: desc=${desc.getClassName()}, typeParams: ${typeParams.contentToString()}") + when (desc) { + // 由于 Byte 的数组有两种方式写入, 需特定读取器 + ByteArraySerializer.descriptor -> { + // ByteArray, 交给 decodeSerializableValue 进行处理 + return this + } + is ListLikeDescriptor -> { + if (typeParams.isNotEmpty() && typeParams[0] is ByteSerializer) { + // Array + return this // 交给 decodeSerializableValue + } + + val tag = currentTagOrNull + @Suppress("SENSELESS_COMPARISON") // 推断 bug + if (tag != null && input.skipToTagOrNull(tag) { + popTag() + if (it.type == SIMPLE_LIST) { + input.readHead() // list 里面元素类型, 没必要知道 + } + return when (it.type) { + SIMPLE_LIST, LIST -> JceListReader(input.readInt(0), this.input) + MAP -> JceMapReader(input.readInt(0), this.input) + else -> error("type mismatch") + } + } == null && desc.isNullable) { + return NullReader(this.input) + } + } + + is MapLikeDescriptor -> { + val tag = currentTagOrNull + + if (tag != null && input.skipToTagOrNull(tag) { popTag() } == null && desc.isNullable) { + return NullReader(this.input) + } + + return JceMapReader(input.readInt(0), this.input) + } + } + + if (desc.kind == StructureKind.CLASS || desc.kind == UnionKind.OBJECT) { + val tag = currentTagOrNull + if (tag != null) { + @Suppress("SENSELESS_COMPARISON") // 推断 bug + if (input.skipToTagOrNull(tag) { + popTag() + return JceStructReader(input) + } == null && desc.isNullable) { + return NullReader(this.input) + } + } + + return this // top-level + } + + if (!input.input.endOfInput) { + val tag = currentTagOrNull + if (tag != null && input.peakHead().tag > tag) { + return NullReader(this.input) + } + } + + return super.beginStructure(desc, *typeParams) + } + @Suppress("UNCHECKED_CAST") - override fun decodeSerializableValue(deserializer: DeserializationStrategy): T = when (deserializer.descriptor) { - is MapLikeDescriptor -> { - deserializer as MapLikeSerializer - - val tag = popTag() - this.input.skipToTagOrNull(tag) { - check(it.type.toInt() == 8) { "type mismatch: ${it.type}" } - val size = this.input.readInt(0) - val map = HashMap(size) - repeat(size) { - pushTag(0) - val key = deserializer.keySerializer.deserialize(this) - pushTag(1) - val value = deserializer.valueSerializer.deserialize(this) - map[key] = value + override fun decodeNullableSerializableValue(deserializer: DeserializationStrategy): T? { + if (deserializer is NullReader) { + return null + } + when (deserializer.descriptor) { + ByteArraySerializer.descriptor -> { + return input.readByteArray(popTag()) as T + } + is ListLikeDescriptor -> { + if (deserializer is ReferenceArraySerializer<*, *> + && (deserializer as ListLikeSerializer).typeParams.isNotEmpty() + && (deserializer as ListLikeSerializer).typeParams[0] is ByteSerializer + ) { + return input.readByteArray(popTag()).toTypedArray() as T + } else if (deserializer is ArrayListSerializer<*> + && (deserializer as ArrayListSerializer<*>).typeParams.isNotEmpty() + && (deserializer as ArrayListSerializer<*>).typeParams[0] is ByteSerializer + ) { + return input.readByteArray(popTag()).toMutableList() as T } - return map as T - } ?: error("cannot find tag $tag") + } + is MapLikeDescriptor -> { + // 将 mapOf(k1 to v1, k2 to v2, ...) 转换为 listOf(k1, v1, k2, v2, ...) 以便于写入. + val serializer = (deserializer as MapLikeSerializer) + val mapEntrySerial = MapEntrySerializer(serializer.keySerializer, serializer.valueSerializer) + val setOfEntries = HashSetSerializer(mapEntrySerial).deserialize(this) + return setOfEntries.associateBy({ it.key }, { it.value }) as T + } } - ByteArraySerializer.descriptor -> input.readByteArray(popTag()) as T - ShortArraySerializer.descriptor -> input.readShortArray(popTag()) as T - IntArraySerializer.descriptor -> input.readIntArray(popTag()) as T - LongArraySerializer.descriptor -> input.readLongArray(popTag()) as T - FloatArraySerializer.descriptor -> input.readFloatArray(popTag()) as T - DoubleArraySerializer.descriptor -> input.readDoubleArray(popTag()) as T - CharArraySerializer.descriptor -> input.readByteArray(popTag()).map { it.toChar() }.toCharArray() as T - BooleanArraySerializer.descriptor -> input.readBooleanArray(popTag()) as T + return super.decodeSerializableValue(deserializer) + } - is ArrayClassDesc -> { - deserializer as ArrayListSerializer - - val tag = popTag() - input.skipToTagOrNull(tag) { head -> - return Array(input.readInt(0)) { - input.readHead() - deserializer.deserialize(this) - } as T - } ?: error("cannot find tag $tag") - } - is ListLikeDescriptor -> { - deserializer as ListLikeSerializer - - val tag = currentTag - input.skipToTagOrNull(tag) { head -> - val size = input.readInt(0) - val list = ArrayList(size) - - repeat(size) { - //input.readHead() - this.pushTag( 0) - list.add(deserializer.typeParams[0].also { println(it.getClassName()) }.deserialize(this)) - } - - return list as T - } ?: error("cannot find tag $tag") - } - else -> { - if (input.peakHead().type.toInt() == STRUCT_BEGIN) { - input.readHead() - deserializer.deserialize(this).also { input.readHead() } - } else deserializer.deserialize(this) - } + @Suppress("UNCHECKED_CAST") + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return decodeNullableSerializableValue(deserializer as DeserializationStrategy) as? T ?: error("value is not optional but cannot find") } override fun decodeTaggedEnum(tag: Int, enumDescription: SerialDescriptor): Int = @@ -606,23 +688,23 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo } } - internal const val BYTE: Int = 0 - internal const val DOUBLE: Int = 5 - internal const val FLOAT: Int = 4 - internal const val INT: Int = 2 + internal const val BYTE: Byte = 0 + internal const val DOUBLE: Byte = 5 + internal const val FLOAT: Byte = 4 + internal const val INT: Byte = 2 internal const val JCE_MAX_STRING_LENGTH = 104857600 - internal const val LIST: Int = 9 - internal const val LONG: Int = 3 - internal const val MAP: Int = 8 - internal const val SHORT: Int = 1 - internal const val SIMPLE_LIST: Int = 13 - internal const val STRING1: Int = 6 - internal const val STRING4: Int = 7 - internal const val STRUCT_BEGIN: Int = 10 - internal const val STRUCT_END: Int = 11 - internal const val ZERO_TYPE: Int = 12 + internal const val LIST: Byte = 9 + internal const val LONG: Byte = 3 + internal const val MAP: Byte = 8 + internal const val SHORT: Byte = 1 + internal const val SIMPLE_LIST: Byte = 13 + internal const val STRING1: Byte = 6 + internal const val STRING4: Byte = 7 + internal const val STRUCT_BEGIN: Byte = 10 + internal const val STRUCT_END: Byte = 11 + internal const val ZERO_TYPE: Byte = 12 - private fun Any?.getClassName(): KClass = if (this == null) Unit::class else this::class + private fun Any?.getClassName(): String = (if (this == null) Unit::class else this::class).simpleName ?: "" } override fun dump(serializer: SerializationStrategy, obj: T): ByteArray { diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/net.mamoe.mirai.qqandroid.io.serialization/JceDecoderTest.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/net.mamoe.mirai.qqandroid.io.serialization/JceDecoderTest.kt index 8f6b3a06f..7055955da 100644 --- a/mirai-core-qqandroid/src/jvmTest/kotlin/net.mamoe.mirai.qqandroid.io.serialization/JceDecoderTest.kt +++ b/mirai-core-qqandroid/src/jvmTest/kotlin/net.mamoe.mirai.qqandroid.io.serialization/JceDecoderTest.kt @@ -1,11 +1,19 @@ package net.mamoe.mirai.qqandroid.io.serialization +import kotlinx.io.core.readBytes import kotlinx.serialization.SerialId import kotlinx.serialization.Serializable +import net.mamoe.mirai.qqandroid.io.JceOutput import net.mamoe.mirai.qqandroid.io.JceStruct +import net.mamoe.mirai.qqandroid.io.buildJcePacket import net.mamoe.mirai.utils.cryptor.contentToString +import net.mamoe.mirai.utils.io.toUHexString import kotlin.test.Test +import kotlin.test.assertEquals +fun main() { + JceDecoderTest().testSimpleMap() +} class JceDecoderTest { @@ -18,23 +26,113 @@ class JceDecoderTest { @SerialId(4) val long: Long = 123, @SerialId(5) val float: Float = 123f, @SerialId(6) val double: Double = 123.0 - ) : JceStruct - - @Test - fun testEncoder() { - println(TestComplexJceStruct().toByteArray(TestComplexJceStruct.serializer()).loadAs(TestComplexJceStruct.serializer()).contentToString()) + ) : JceStruct { + override fun writeTo(output: JceOutput) = output.run { + writeString(string, 0) + writeByte(byte, 1) + writeShort(short, 2) + writeInt(int, 3) + writeLong(long, 4) + writeFloat(float, 5) + writeDouble(double, 6) + } } - @Test - fun testEncoder2() { - - } @Serializable class TestComplexJceStruct( @SerialId(7) val byteArray: ByteArray = byteArrayOf(1, 2, 3), - @SerialId(8) val byteList: List = listOf(1, 2, 3), - @SerialId(9) val map: Map = mapOf("哈哈" to "嘿嘿"), - @SerialId(10) val nestedJceStruct: TestSimpleJceStruct = TestSimpleJceStruct() + @SerialId(8) val byteList: List = listOf(1, 2, 3), // error here + @SerialId(9) val map: Map> = mapOf("哈哈" to mapOf("哈哈" to byteArrayOf(1, 2, 3))), + // @SerialId(10) val nestedJceStruct: TestSimpleJceStruct = TestSimpleJceStruct(), + @SerialId(11) val byteList2: List> = listOf(listOf(1, 2, 3), listOf(1, 2, 3)) ) : JceStruct + + @Serializable + class TestComplexNullableJceStruct( + @SerialId(7) val byteArray: ByteArray? = byteArrayOf(1, 2, 3), + @SerialId(8) val byteList: List? = listOf(1, 2, 3), // error here + @SerialId(9) val map: Map>? = mapOf("哈哈" to mapOf("哈哈" to byteArrayOf(1, 2, 3))), + @SerialId(10) val nestedJceStruct: TestSimpleJceStruct? = TestSimpleJceStruct(), + @SerialId(11) val byteList2: List>? = listOf(listOf(1, 2, 3), listOf(1, 2, 3)) + ) : JceStruct + + @Test + fun testEncoder() { + println(TestComplexJceStruct().toByteArray(TestComplexJceStruct.serializer()).loadAs(TestComplexNullableJceStruct.serializer()).contentToString()) + } + + @Test + fun testEncoder2() { + assertEquals( + buildJcePacket { + writeFully(byteArrayOf(1, 2, 3), 7) + writeCollection(listOf(1, 2, 3), 8) + writeMap(mapOf("哈哈" to mapOf("哈哈" to byteArrayOf(1, 2, 3))), 9) + writeJceStruct(TestSimpleJceStruct(), 10) + writeCollection(listOf(listOf(1, 2, 3), listOf(1, 2, 3)), 11) + }.readBytes().toUHexString(), + TestComplexJceStruct().toByteArray(TestComplexJceStruct.serializer()).toUHexString() + ) + } + + + @Test + fun testNestedList() { + @Serializable + class TestNestedList( + @SerialId(7) val array: List> = listOf(listOf(1, 2, 3), listOf(1, 2, 3), listOf(1, 2, 3)) + ) + + println(buildJcePacket { + writeCollection(listOf(listOf(1, 2, 3), listOf(1, 2, 3), listOf(1, 2, 3)), 7) + }.readBytes().loadAs(TestNestedList.serializer()).contentToString()) + } + + @Test + fun testNestedArray() { + @Serializable + class TestNestedArray( + @SerialId(7) val array: Array> = arrayOf(arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3)) + ) + + println(buildJcePacket { + writeFully(arrayOf(arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3)), 7) + }.readBytes().loadAs(TestNestedArray.serializer()).contentToString()) + } + + @Test + fun testSimpleMap() { + + @Serializable + class TestSimpleMap( + @SerialId(7) val map: Map = mapOf("byteArrayOf(1)" to 2222L) + ) + println(buildJcePacket { + writeMap(mapOf("byteArrayOf(1)" to 2222), 7) + }.readBytes().loadAs(TestSimpleMap.serializer()).contentToString()) + } + + @Test + fun testSimpleList() { + + @Serializable + class TestSimpleList( + @SerialId(7) val list: List = listOf("asd", "asdasdasd") + ) + println(buildJcePacket { + writeCollection(listOf("asd", "asdasdasd"), 7) + }.readBytes().loadAs(TestSimpleList.serializer()).contentToString()) + } + + @Test + fun testNestedMap() { + @Serializable + class TestNestedMap( + @SerialId(7) val map: Map> = mapOf(byteArrayOf(1) to mapOf(byteArrayOf(1) to shortArrayOf(2))) + ) + println(buildJcePacket { + writeMap(mapOf(byteArrayOf(1) to mapOf(byteArrayOf(1) to shortArrayOf(2))), 7) + }.readBytes().loadAs(TestNestedMap.serializer()).map.entries.first().value!!.contentToString()) + } } \ No newline at end of file