Commit 2e80d330 authored by Him188's avatar Him188

JceStruct serialization

parent 46864302
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 <K, V> jceMap(tag: Int, vararg entries: Pair<K, V>): 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 <T> writeFully(src: Array<T>, 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 <K, V> writeMap(map: Map<K, V>, 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 <T> 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<out Any> = 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
package net.mamoe.mirai.qqandroid.io package net.mamoe.mirai.qqandroid.io
interface JceStruct interface JceStruct {
\ No newline at end of file fun writeTo(output: JceOutput) = Unit
}
\ No newline at end of file
package net.mamoe.mirai.qqandroid.io.serialization package net.mamoe.mirai.qqandroid.io.serialization
import kotlinx.io.core.readBytes
import kotlinx.serialization.SerialId import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.JceOutput
import net.mamoe.mirai.qqandroid.io.JceStruct 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.cryptor.contentToString
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
fun main() {
JceDecoderTest().testSimpleMap()
}
class JceDecoderTest { class JceDecoderTest {
...@@ -18,23 +26,113 @@ class JceDecoderTest { ...@@ -18,23 +26,113 @@ class JceDecoderTest {
@SerialId(4) val long: Long = 123, @SerialId(4) val long: Long = 123,
@SerialId(5) val float: Float = 123f, @SerialId(5) val float: Float = 123f,
@SerialId(6) val double: Double = 123.0 @SerialId(6) val double: Double = 123.0
) : 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)
}
}
@Serializable
class TestComplexJceStruct(
@SerialId(7) val byteArray: ByteArray = byteArrayOf(1, 2, 3),
@SerialId(8) val byteList: List<Byte> = listOf(1, 2, 3), // error here
@SerialId(9) val map: Map<String, Map<String, ByteArray>> = mapOf("哈哈" to mapOf("哈哈" to byteArrayOf(1, 2, 3))),
// @SerialId(10) val nestedJceStruct: TestSimpleJceStruct = TestSimpleJceStruct(),
@SerialId(11) val byteList2: List<List<Int>> = 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<Byte>? = listOf(1, 2, 3), // error here
@SerialId(9) val map: Map<String, Map<String, ByteArray>>? = mapOf("哈哈" to mapOf("哈哈" to byteArrayOf(1, 2, 3))),
@SerialId(10) val nestedJceStruct: TestSimpleJceStruct? = TestSimpleJceStruct(),
@SerialId(11) val byteList2: List<List<Int>>? = listOf(listOf(1, 2, 3), listOf(1, 2, 3))
) : JceStruct ) : JceStruct
@Test @Test
fun testEncoder() { fun testEncoder() {
println(TestComplexJceStruct().toByteArray(TestComplexJceStruct.serializer()).loadAs(TestComplexJceStruct.serializer()).contentToString()) println(TestComplexJceStruct().toByteArray(TestComplexJceStruct.serializer()).loadAs(TestComplexNullableJceStruct.serializer()).contentToString())
} }
@Test @Test
fun testEncoder2() { 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<List<Int>> = 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())
} }
@Serializable @Test
class TestComplexJceStruct( fun testNestedArray() {
@SerialId(7) val byteArray: ByteArray = byteArrayOf(1, 2, 3), @Serializable
@SerialId(8) val byteList: List<Byte> = listOf(1, 2, 3), class TestNestedArray(
@SerialId(9) val map: Map<String, String> = mapOf("哈哈" to "嘿嘿"), @SerialId(7) val array: Array<Array<Int>> = arrayOf(arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3))
@SerialId(10) val nestedJceStruct: TestSimpleJceStruct = TestSimpleJceStruct() )
) : JceStruct
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<String, Long> = 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<String> = 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<ByteArray, Map<ByteArray, ShortArray>> = 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment