Implement multi versioned DeviceInfo, implement DeviceInfo v2 which stores properties as String and hex strings instead of ByteArrays.

This commit is contained in:
Him188 2021-11-28 17:58:36 +00:00
parent d5d0b35806
commit 8b99cc45fb
2 changed files with 309 additions and 3 deletions

View File

@ -10,11 +10,17 @@
package net.mamoe.mirai.utils
import kotlinx.io.core.toByteArray
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans
import java.io.File
import kotlin.random.Random
@ -98,14 +104,14 @@ public class DeviceInfo(
@JvmStatic
@JvmName("from")
public fun File.loadAsDeviceInfo(
json: Json = Json
json: Json = DeviceInfoManager.format
): DeviceInfo {
if (!this.exists() || this.length() == 0L) {
return random().also {
this.writeText(json.encodeToString(serializer(), it))
this.writeText(DeviceInfoManager.serialize(it, json))
}
}
return json.decodeFromString(serializer(), this.readText())
return DeviceInfoManager.deserialize(this.readText(), json)
}
/**
@ -245,6 +251,205 @@ public fun DeviceInfo.generateDeviceInfoData(): ByteArray {
)
}
internal object DeviceInfoManager {
sealed interface Info {
fun toDeviceInfo(): DeviceInfo
}
@Serializable(HexStringSerializer::class)
@JvmInline
value class HexString(
val data: ByteArray
)
object HexStringSerializer : KSerializer<HexString> by String.serializer().map(
String.serializer().descriptor.copy("HexString"),
deserialize = { HexString(it.hexToBytes()) },
serialize = { it.data.toUHexString("").lowercase() }
)
// Note: property names must be kept intact during obfuscation process if applied.
@Serializable
class Wrapper<T : Info>(
@Suppress("unused") val deviceInfoVersion: Int, // used by plain jsonObject
val data: T
)
@Serializable
class V1(
val display: ByteArray,
val product: ByteArray,
val device: ByteArray,
val board: ByteArray,
val brand: ByteArray,
val model: ByteArray,
val bootloader: ByteArray,
val fingerprint: ByteArray,
val bootId: ByteArray,
val procVersion: ByteArray,
val baseBand: ByteArray,
val version: DeviceInfo.Version,
val simInfo: ByteArray,
val osType: ByteArray,
val macAddress: ByteArray,
val wifiBSSID: ByteArray,
val wifiSSID: ByteArray,
val imsiMd5: ByteArray,
val imei: String,
val apn: ByteArray
) : Info {
override fun toDeviceInfo(): DeviceInfo {
return DeviceInfo(
display = display,
product = product,
device = device,
board = board,
brand = brand,
model = model,
bootloader = bootloader,
fingerprint = fingerprint,
bootId = bootId,
procVersion = procVersion,
baseBand = baseBand,
version = version,
simInfo = simInfo,
osType = osType,
macAddress = macAddress,
wifiBSSID = wifiBSSID,
wifiSSID = wifiSSID,
imsiMd5 = imsiMd5,
imei = imei,
apn = apn
)
}
}
@Serializable
class V2(
val display: String,
val product: String,
val device: String,
val board: String,
val brand: String,
val model: String,
val bootloader: String,
val fingerprint: String,
val bootId: String,
val procVersion: String,
val baseBand: HexString,
val version: Version,
val simInfo: String,
val osType: String,
val macAddress: String,
val wifiBSSID: String,
val wifiSSID: String,
val imsiMd5: HexString,
val imei: String,
val apn: String
) : Info {
override fun toDeviceInfo(): DeviceInfo = DeviceInfo(
this.display.toByteArray(),
this.product.toByteArray(),
this.device.toByteArray(),
this.board.toByteArray(),
this.brand.toByteArray(),
this.model.toByteArray(),
this.bootloader.toByteArray(),
this.fingerprint.toByteArray(),
this.bootId.toByteArray(),
this.procVersion.toByteArray(),
this.baseBand.data,
this.version.trans(),
this.simInfo.toByteArray(),
this.osType.toByteArray(),
this.macAddress.toByteArray(),
this.wifiBSSID.toByteArray(),
this.wifiSSID.toByteArray(),
this.imsiMd5.data,
this.imei,
this.apn.toByteArray()
)
}
@Serializable
class Version(
val incremental: String,
val release: String,
val codename: String,
val sdk: Int = 29
) {
companion object {
fun DeviceInfo.Version.trans(): Version {
return Version(incremental.decodeToString(), release.decodeToString(), codename.decodeToString(), sdk)
}
fun Version.trans(): DeviceInfo.Version {
return DeviceInfo.Version(incremental.toByteArray(), release.toByteArray(), codename.toByteArray(), sdk)
}
}
}
fun DeviceInfo.toCurrentInfo(): V2 = V2(
display.decodeToString(),
product.decodeToString(),
device.decodeToString(),
board.decodeToString(),
brand.decodeToString(),
model.decodeToString(),
bootloader.decodeToString(),
fingerprint.decodeToString(),
bootId.decodeToString(),
procVersion.decodeToString(),
HexString(baseBand),
version.trans(),
simInfo.decodeToString(),
osType.decodeToString(),
macAddress.decodeToString(),
wifiBSSID.decodeToString(),
wifiSSID.decodeToString(),
HexString(imsiMd5),
imei,
apn.decodeToString()
)
internal val format = Json {
ignoreUnknownKeys = true
isLenient = true
}
@Throws(IllegalArgumentException::class, NumberFormatException::class) // in case malformed
fun deserialize(string: String, format: Json = this.format): DeviceInfo {
val element = format.parseToJsonElement(string)
return when (val version = element.jsonObject["deviceInfoVersion"]?.jsonPrimitive?.content?.toInt() ?: 1) {
/**
* @since 2.0
*/
1 -> format.decodeFromJsonElement(V1.serializer(), element)
/**
* @since 2.9
*/
2 -> format.decodeFromJsonElement(Wrapper.serializer(V2.serializer()), element).data
else -> throw IllegalArgumentException("Unsupported deviceInfoVersion: $version")
}.toDeviceInfo()
}
fun serialize(info: DeviceInfo, format: Json = this.format): String {
return format.encodeToString(
Wrapper.serializer(V2.serializer()),
Wrapper(2, info.toCurrentInfo())
)
}
fun toJsonElement(info: DeviceInfo, format: Json = this.format): JsonElement {
return format.encodeToJsonElement(
Wrapper.serializer(V2.serializer()),
Wrapper(2, info.toCurrentInfo())
)
}
}
/**
* Defaults "%4;7t>;28<fc.5*6".toByteArray()
*/

View File

@ -9,9 +9,17 @@
package net.mamoe.mirai.utils
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
import org.junit.jupiter.api.io.TempDir
import java.io.File
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class DeviceInfoTest {
@Test
@ -19,4 +27,97 @@ class DeviceInfoTest {
val time = System.currentTimeMillis()
assertEquals(DeviceInfo.random(Random(time)), DeviceInfo.random(Random(time)))
}
class HexStringTest {
@Test
fun `can serialize as String`() {
val hexString = DeviceInfoManager.HexString(byteArrayOf(1, 2))
val string = Json.encodeToString(DeviceInfoManager.HexString.serializer(), hexString)
assertEquals("\"0102\"", string)
}
@Test
fun `can deserialize from String`() {
val hex = Json.decodeFromString(DeviceInfoManager.HexString.serializer(), "\"0102\"")
assertContentEquals(byteArrayOf(1, 2), hex.data)
}
}
@TempDir
lateinit var dir: File
@Test
fun `can serialize and deserialize v2`() {
val device = DeviceInfo.random()
assertEquals(device, DeviceInfoManager.deserialize(DeviceInfoManager.serialize(device)))
}
@Test
fun `can write and read v2`() {
val device = DeviceInfo.random()
val file = dir.resolve("device.json")
file.writeText(DeviceInfoManager.serialize(device))
assertEquals(device, file.loadAsDeviceInfo())
}
@Test
fun `current version pretty print preview`() {
val device = DeviceInfo.random()
val text = DeviceInfoManager.serialize(device, Json {
prettyPrint = true
})
println(text)
/*
{
"deviceInfoVersion": 2,
"data": {
"display": "MIRAI.868912.001",
"product": "mirai",
"device": "mirai",
"board": "mirai",
"brand": "mamoe",
"model": "mirai",
"bootloader": "unknown",
"fingerprint": "mamoe/mirai/mirai:10/MIRAI.200122.001/6174518:user/release-keys",
"bootId": "500E9D6F-1A76-4ED0-20F3-66A5B20C7049",
"procVersion": "Linux version 3.0.31-r35YRB94 (android-build@xxx.xxx.xxx.xxx.com)",
"baseBand": "",
"version": {
"incremental": "5891938",
"release": "10",
"codename": "REL"
},
"simInfo": "T-Mobile",
"osType": "android",
"macAddress": "02:00:00:00:00:00",
"wifiBSSID": "02:00:00:00:00:00",
"wifiSSID": "<unknown ssid>",
"imsiMd5": "d1ead821747a3ad3f8f3784fafa3b954",
"imei": "155970036849035",
"apn": "wifi"
}
}
*/
val element = DeviceInfoManager.toJsonElement(device)
assertEquals(2, element.jsonObject["deviceInfoVersion"]!!.jsonPrimitive.content.toInt())
val imsiMd5 = element.jsonObject["data"]!!.jsonObject["imsiMd5"]!!.jsonPrimitive.content
assertEquals(
device.imsiMd5.toUHexString("").lowercase(),
imsiMd5
)
assertTrue { imsiMd5 matches Regex("""[a-z0-9]+""") }
}
@Test
fun `can read legacy v1`() {
val device = DeviceInfo.random()
val file = dir.resolve("device.json")
file.writeText(Json.encodeToString(DeviceInfo.serializer(), device))
assertEquals(device, file.loadAsDeviceInfo())
}
}