diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt new file mode 100644 index 000000000..b69d63923 --- /dev/null +++ b/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt @@ -0,0 +1,368 @@ +package test + +import net.mamoe.mirai.utils.cryptor.ProtoType +import net.mamoe.mirai.utils.cryptor.protoFieldNumber +import java.io.File + +fun main() { + println( + File("""E:\Projects\QQAndroidFF\app\src\main\java\msf\onlinepush""") + .generateUnarrangedClasses().toMutableList().arrangeClasses().joinToString("\n\n") + ) +} + +class ArrangedClass( + val name: String, + val source: String, + val innerClasses: MutableList<ArrangedClass> +) { + fun toKotlinProtoBufClass(): String { + return if (innerClasses.isNotEmpty()) { + """ + $source { + ${innerClasses.joinToString("\n\n") { " ${it.toKotlinProtoBufClass()}" }} + } + """.trimIndent() + } else source + } + + override fun toString(): String = toKotlinProtoBufClass() +} + +fun MutableList<GeneratedClass>.arrangeClasses(): List<ArrangedClass> { + val tree: MutableMap<String, ArrangedClass> = mutableMapOf() + + // 先处理所有没有父类的 + this.removeAll(this.filter { it.superclasses.isEmpty() }.onEach { + tree[it.className] = it.toArrangedClass() + }) + + // 一层继承的处理 + tree.forEach { (name, clazz) -> + this.removeAll(this.filter { it.superclasses.size == 1 && it.superclasses[0] == name }.onEach { + clazz.innerClasses.add(it.toArrangedClass()) + }) + } + + // 两层继承的处理 + tree.filter { it.value.innerClasses.isNotEmpty() }.forEach { (_, clazz) -> + clazz.innerClasses.forEach { innerClass -> + this.removeAll(this.filter { it.superclasses[1] == innerClass.name }.onEach { + innerClass.innerClasses.add(it.toArrangedClass()) + }) + } + } + + return tree.values.toList() + +// // 循环处理每个 class 的第一个父类 +// while (this.any { it.superclasses.isNotEmpty() }) { +// this.forEach { generatedClass: GeneratedClass -> +// generatedClass.superclasses.lastOrNull()?.let { superClassName -> +// if (!tree.containsKey(superClassName)) { +// tree[superClassName] = this@arrangeClasses.firstOrNull { +// it.className == superClassName +// } ?: inline { +// println("${generatedClass.className} 继承了 $superClassName, 但找不到生成的这个 class, 将使用一个空的 class 替代") +// GeneratedClass(mutableListOf(), superClassName, "class $superClassName") +// } +// } +// +// generatedClass.superclasses.remove(superClassName) +// } +// } +// } +} + +fun File.generateUnarrangedClasses(): List<GeneratedClass> { + return this.listFiles()?.filter { it.isFile }?.map { + it.readText().generateProtoBufDataClass() + } ?: error("Not a folder") +} + +fun String.substringBetween(left: String, right: String): String { + return this.substringAfter(left, "").substringBefore(right, "") +} + +data class GeneratedClass( + /** + * 带先后顺序 + */ + val superclasses: MutableList<String>, + val className: String, + val source: String +) { + fun toArrangedClass(): ArrangedClass { + return ArrangedClass(className, source, mutableListOf()) + } +} + +sealed class InferredType(val adjustKotlinAnnotationDeclaration: (String) -> String = { it }, val kotlinType: String) { + object FIXED64 : InferredType({ "@ProtoType(ProtoNumberType.FIXED) $it" }, "Long") + object FIXED32 : InferredType({ "@ProtoType(ProtoNumberType.FIXED) $it" }, "Int") + object FIXED16 : InferredType({ "@ProtoType(ProtoNumberType.FIXED) $it" }, "Short") + object FIXED8 : InferredType({ "@ProtoType(ProtoNumberType.FIXED) $it" }, "Byte") + + object SIGNED64 : InferredType({ "@ProtoType(ProtoNumberType.SIGNED) $it" }, "Long") + object SIGNED32 : InferredType({ "@ProtoType(ProtoNumberType.SIGNED) $it" }, "Int") + object SIGNED16 : InferredType({ "@ProtoType(ProtoNumberType.SIGNED) $it" }, "Short") + object SIGNED8 : InferredType({ "@ProtoType(ProtoNumberType.SIGNED) $it" }, "Byte") + + object UNSIGNED64 : InferredType(kotlinType = "Long") + object UNSIGNED32 : InferredType(kotlinType = "Int") + object UNSIGNED16 : InferredType(kotlinType = "Short") + object UNSIGNED8 : InferredType(kotlinType = "Byte") + + object BYTES : InferredType(kotlinType = "ByteArray") + object STRING : InferredType(kotlinType = "String") + + object FLOAT : InferredType(kotlinType = "Float") + object DOUBLE : InferredType(kotlinType = "Double") + object BOOLEAN : InferredType(kotlinType = "Boolean") + + object ENUM : InferredType(kotlinType = "Int /* enum */") + + class REPEATED(kotlinType: String) : InferredType(kotlinType = "List<$kotlinType>") + class CUSTOM(kotlinType: String) : InferredType(kotlinType = kotlinType) +} + +data class PBFieldInfo( + val protoTag: Int, + val name: String, + val inferredType: InferredType, + val defaultValue: String +) { + fun toKotlinProtoBufClassParam(): String { + return "${inferredType.adjustKotlinAnnotationDeclaration("@SerialId($protoTag)")} val $name: ${inferredType.kotlinType}${if (defaultValue == "null") "?" else ""} = $defaultValue" + } +} + +@UseExperimental(ExperimentalUnsignedTypes::class) +fun String.generateProtoBufDataClass(): GeneratedClass { + if (this.indexOf("extends") == -1) { + val javaClassname = substringBetween("class", "{") + val superclasses = javaClassname.split("$").map { it.trim().adjustClassName() }.toMutableList().apply { removeAt(this.lastIndex) } + val className = substringBetween("class", "{").substringAfterLast("$").trim().adjustClassName() + return GeneratedClass(superclasses, className, "@Serializable\nclass $className") + } + + val superclasses = substringBetween("class", "extends").split("$").map { it.trim().adjustClassName() }.toMutableList() + superclasses.removeAt(superclasses.lastIndex) + val className = substringBetween("class", "extends").substringAfterLast("$").trim().adjustClassName() + + + val ids = substringBetween("new int[]{", "}").split(",").map { it.trim() } + + if (ids.all { it.isBlank() }) { + return GeneratedClass(superclasses, className, "@Serializable\nclass $className") + } + + val names = substringBetween("new String[]{", "}").split(",").map { it.trim() } + val defaultValues = substringBetween("new Object[]{", "}").split(",").map { it.trim() } + + + // name to original declaration + val pbTypedFields = lines() + .asSequence() + .map { it.trim() } + .filter { it.startsWith("public final PB") } + .filterNot { it.startsWith("public final class") } + .map { it.substringAfter("public final PB") } + .associateBy { it.substringBetween(" ", " ").takeIf { it.isNotBlank() } ?: it.substringBetween(" ", ";") } + .mapKeys { it.key.trim() } + + val customTypedFields = lines() + .asSequence() + .map { it.trim() } + .filter { it.startsWith("public ") } + .filterNot { it.startsWith("public final") } + .filterNot { it.startsWith("public static") } + .filterNot { it.startsWith("public ${substringBetween("class", "extends").trim()}()") } + .map { it.substringAfter("public ") } + .associateBy { it.substringBetween(" ", " ").takeIf { it.isNotBlank() } ?: it.substringBetween(" ", ";") } + .mapKeys { it.key.trim() } + + val source = buildString { + append("@Serializable").append("\n") + append("class $className(").append("\n") + + ids.map { it.toUInt() } + .map { protoFieldNumber(it) } + .mapIndexed { index, tag -> + var name = names[index].adjustName() + var defaultValue = defaultValues[index].let { + if (it.startsWith("var")) "EMPTY_BYTE_ARRAY" else it + } + + val originalName = names[index].substringBetween("\"", "\"") + val javaDeclaration = pbTypedFields[originalName] + + val inferredType: InferredType = if (javaDeclaration == null) { + InferredType.CUSTOM( + customTypedFields[originalName]?.substringBefore(" ")?.adjustClassName()?.replace("$", ".") + ?: error("找不到 customTypedFields for $originalName in class $className") + ) + } else { + when (val javaFieldType = javaDeclaration.substringBefore("Field")) { + "Int8", "UInt8" -> InferredType.UNSIGNED8 + "Int16", "UInt16" -> InferredType.UNSIGNED16 + "Int32", "UInt32" -> InferredType.UNSIGNED32 + "Int64", "UInt64" -> InferredType.UNSIGNED64 + + "SInt8" -> InferredType.SIGNED8 + "SInt16" -> InferredType.SIGNED16 + "SInt32" -> InferredType.SIGNED32 + "SInt64" -> InferredType.SIGNED64 + + "Fixed8", "FInt8", "SFInt8" -> InferredType.FIXED8 + "Fixed16", "FInt16", "SFInt16" -> InferredType.FIXED16 + "Fixed32", "FInt32", "SFInt32" -> InferredType.FIXED32 + "Fixed64", "FInt64", "SFInt64" -> InferredType.FIXED64 + + "Bytes" -> InferredType.BYTES + "String" -> InferredType.STRING + "Double" -> InferredType.DOUBLE + "Float" -> InferredType.FLOAT + "Bool" -> InferredType.BOOLEAN + "Enum" -> InferredType.ENUM + + "Repeat", "RepeatMessage" -> InferredType.REPEATED( + javaDeclaration.substringBetween("<", ">").adjustClassName().replace( + "$", + "." + ).replace("Integer", "Int") + ) + else -> error("Unsupported type: $javaFieldType for $originalName in class $className") + } + } + + fun adjustPropertyName(_name: String): String { + var name = _name + when { + name.startsWith("str") -> { + name = name.substringAfter("str").takeIf { it.isNotBlank() }?.adjustName() ?: "str" + if (defaultValue == "EMPTY_BYTE_ARRAY") + defaultValue = "\"\"" + } + name.startsWith("uint32") -> { + name = name.substringAfter("uint32").takeIf { it.isNotBlank() }?.adjustName() ?: "uint32" + defaultValue = defaultValue.replace("D", "", ignoreCase = true) + .replace("f", "", ignoreCase = true) + .replace(".0", "", ignoreCase = true) + .replace("l", "", ignoreCase = true) + } + name.startsWith("double") -> { + name = name.substringAfter("double").takeIf { it.isNotBlank() }?.adjustName() ?: "double" + defaultValue = defaultValue.replace("D", "", ignoreCase = true) + .replace("f", "", ignoreCase = true) + .replace("l", "", ignoreCase = true) + } + name.startsWith("float") -> { + name = name.substringAfter("float").takeIf { it.isNotBlank() }?.adjustName() ?: "float" + defaultValue = defaultValue.replace("D", "", ignoreCase = true) + .replace("f", "", ignoreCase = true) + .replace("l", "", ignoreCase = true) + "f" + } + name.startsWith("uint16") -> { + name = name.substringAfter("uint16").takeIf { it.isNotBlank() }?.adjustName() ?: "uint16" + defaultValue = defaultValue.replace("D", "", ignoreCase = true) + .replace("f", "", ignoreCase = true) + .replace(".0", "", ignoreCase = true) + .replace("l", "", ignoreCase = true) + } + name.startsWith("uint8") -> { + name = name.substringAfter("uint8").takeIf { it.isNotBlank() }?.adjustName() ?: "uint8" + defaultValue = defaultValue.replace("D", "", ignoreCase = true) + .replace("f", "", ignoreCase = true) + .replace(".0", "", ignoreCase = true) + .replace("l", "", ignoreCase = true) + } + name.startsWith("uint64") -> { + name = name.substringAfter("uint64").takeIf { it.isNotBlank() }?.adjustName() ?: "uint64" + defaultValue = defaultValue.replace("D", "", ignoreCase = true) + .replace("f", "", ignoreCase = true) + .replace(".0", "", ignoreCase = true) + } + name.startsWith("bytes") -> { + name = name.substringAfter("bytes").takeIf { it.isNotBlank() }?.adjustName() ?: "bytes" + } + name.startsWith("rpt") -> { + name = name.substringAfter("rpt").takeIf { it.isNotBlank() }?.substringAfter("_")?.let { adjustPropertyName(it) } ?: "rpt" + } + } + return name + } + name = adjustPropertyName(name) + + when (inferredType) { + is InferredType.REPEATED -> { + if (defaultValue.getNumericalValue() == 0 || defaultValue == "EMPTY_BYTE_ARRAY") { + defaultValue = "null" + } + } + is InferredType.STRING -> { + if (defaultValue == "EMPTY_BYTE_ARRAY") { + defaultValue = "\"\"" + } + } + is InferredType.BYTES -> { + if (defaultValue == "\"\"") { + defaultValue = "EMPTY_BYTE_ARRAY" + } + } + } + + name = name.adjustName() + if (name[0] in '0'..'9') { + name = "_" + name + } + + append(PBFieldInfo(tag, name, inferredType, defaultValue).toKotlinProtoBufClassParam()) + + if (ids.size - 1 != index) { + append(",") + } + + append("\n") + } + + append(")") + } + + return GeneratedClass(superclasses, className, source) +} + +fun String.getNumericalValue(): Int? { + return this.filter { it in '0'..'9' }.toDoubleOrNull()?.toInt() +} + +fun ProtoType.mapToKotlinType(): String { + return when (this) { + ProtoType.VAR_INT -> "Int" + ProtoType.BIT_64 -> "Long" + ProtoType.LENGTH_DELIMI -> "String" + ProtoType.BIT_32 -> "Float" + else -> "UNKNOWN" + } +} + +fun String.adjustClassName(): String { + when (this.trim()) { + "ByteStringMicro" -> return "ByteArray" + } + return String(this.adjustName().toCharArray().apply { this[0] = this[0].toUpperCase() }) +} + +fun String.adjustName(): String { + val result = this.toCharArray() + result[0] = result[0].toLowerCase() + for (index in result.indices) { + if (result[index] == '_') { + if (index + 1 in result.indices) { + result[index + 1] = result[index + 1].toUpperCase() + } + } + } + + return String(result).replace("_", "").trim().removePrefix("\"").removeSuffix("\"") +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/test/QLogReader.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/test/QLogReader.kt new file mode 100644 index 000000000..350ad7079 --- /dev/null +++ b/mirai-core-qqandroid/src/jvmTest/kotlin/test/QLogReader.kt @@ -0,0 +1,55 @@ +package test + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.util.zip.InflaterInputStream + +object QLogReader { + @JvmStatic + fun main(args: Array<String>) { + + println(readQLog(File("C:\\Users\\Him18\\Desktop\\log\\wtlogin_20200101.log"))) + } + + fun readQLog(file: File): String { + return (decompress(file.readBytes())) + } + + + fun decompress(array: ByteArray): String { + return buildString { + if (array.isNotEmpty()) { + var i = 0 + var n = 0 + while (array.size > n + 3) { + val buf_to_int32: Int = buf_to_int32(array, n) + if (array.size <= n + buf_to_int32 + 3) { + break + } + val buf = ByteArray(buf_to_int32) + System.arraycopy(array, n + 4, buf, 0, buf_to_int32) + n += 4 + buf_to_int32 + ++i + val byteArrayOutputStream = ByteArrayOutputStream() + val `in` = ByteArrayInputStream(buf) + val inflaterInputStream = InflaterInputStream(`in`) + val array2 = ByteArray(1024) + while (true) { + val read = inflaterInputStream.read(array2) + if (read == -1) { + break + } + byteArrayOutputStream.write(array2, 0, read) + } + append(byteArrayOutputStream.toString()) + } + } + } + + } + + private fun buf_to_int32(array: ByteArray, n: Int): Int { + return (array[n].toInt() shl 24 and -0x1000000) + (array[n + 1].toInt() shl 16 and 0xFF0000) + (array[n + 2].toInt() shl 8 and 0xFF00) + (array[n + 3].toInt() shl 0 and 0xFF) + } +} \ No newline at end of file