From 243b2ea7318441e2b92e9f46b84ffc232bc265cf Mon Sep 17 00:00:00 2001
From: Him188 <Him188@mamoe.net>
Date: Sun, 8 Mar 2020 21:14:41 +0800
Subject: [PATCH] Gather all platform-specified utilities into a
 `MiraiPlatformUtils`

---
 .../net/mamoe/mirai/qqandroid/ContactImpl.kt  |   3 +-
 .../net/mamoe/mirai/qqandroid/QQAndroidBot.kt |   2 +-
 .../mamoe/mirai/qqandroid/message/messages.kt |  42 ++++-
 .../qqandroid/network/QQAndroidClient.kt      |   3 +-
 .../qqandroid/network/highway/highway.kt      |   3 +-
 .../network/protocol/packet/PacketFactory.kt  |   2 +-
 .../qqandroid/network/protocol/packet/Tlv.kt  |  15 +-
 .../network/protocol/packet/login/StatSvc.kt  |   6 +-
 .../network/protocol/packet/login/WtLogin.kt  |   2 +-
 .../mamoe/mirai/utils/ExternalImageAndroid.kt |   3 +-
 .../net/mamoe/mirai/utils/SystemDeviceInfo.kt |   5 +
 .../mamoe/mirai/utils/cryptor/ECDHAndroid.kt  |   4 +-
 .../net/mamoe/mirai/utils/platformAndroid.kt  | 159 +++++++++---------
 .../kotlin/net.mamoe.mirai/BotAccount.kt      |   4 +-
 .../net.mamoe.mirai/utils/DeviceInfo.kt       |   8 +-
 .../kotlin/net.mamoe.mirai/utils/platform.kt  |  37 ++--
 .../net/mamoe/mirai/utils/ExternalImageJvm.kt |   5 +-
 .../net/mamoe/mirai/utils/PlatformUtilsJvm.kt | 145 +++++++++-------
 .../net/mamoe/mirai/utils/SystemDeviceInfo.kt |   4 +-
 .../net/mamoe/mirai/utils/cryptor/ECDHJvm.kt  |   6 +-
 20 files changed, 271 insertions(+), 187 deletions(-)

diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
index b73860daa..d67a04053 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
@@ -78,6 +78,7 @@ internal class QQImpl(
         return MessageReceipt(source, this, null)
     }
 
+    @OptIn(MiraiInternalAPI::class)
     override suspend fun uploadImage(image: ExternalImage): OfflineFriendImage = try {
         if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
             throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
@@ -111,7 +112,7 @@ internal class QQImpl(
                     ImageUploadEvent.Succeed(this@QQImpl, image, it).broadcast()
                 }
                 is LongConn.OffPicUp.Response.RequireUpload -> {
-                    Http.postImage(
+                    MiraiPlatformUtils.Http.postImage(
                         "0x6ff0070",
                         bot.uin,
                         null,
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
index af7b7b02f..b87883f51 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
@@ -222,7 +222,7 @@ internal abstract class QQAndroidBotBase constructor(
     }
 
     override suspend fun openChannel(image: Image): ByteReadChannel {
-        return Http.get<HttpResponse>(queryImageUrl(image)).content.toKotlinByteReadChannel()
+        return MiraiPlatformUtils.Http.get<HttpResponse>(queryImageUrl(image)).content.toKotlinByteReadChannel()
     }
 }
 
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt
index 7a64a5a6e..51a8e7451 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt
@@ -9,18 +9,14 @@
 
 package net.mamoe.mirai.qqandroid.message
 
-import kotlinx.io.core.buildPacket
-import kotlinx.io.core.discardExact
-import kotlinx.io.core.readBytes
-import kotlinx.io.core.readUInt
+import kotlinx.io.core.*
 import net.mamoe.mirai.LowLevelAPI
 import net.mamoe.mirai.contact.Member
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
-import net.mamoe.mirai.utils.ExternalImage
-import net.mamoe.mirai.utils.MiraiDebugAPI
-import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.*
+import net.mamoe.mirai.utils.io.encodeToString
 import net.mamoe.mirai.utils.io.hexToBytes
 import net.mamoe.mirai.utils.io.read
 import net.mamoe.mirai.utils.io.toByteArray
@@ -222,7 +218,7 @@ private val atAllData = ImMsgBody.Elem(
     )
 )
 
-@OptIn(MiraiInternalAPI::class)
+@OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
 internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgBody.Elem> {
     val elements = mutableListOf<ImMsgBody.Elem>()
 
@@ -243,6 +239,14 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
                 elements.add(ImMsgBody.Elem(text = it.toJceData()))
                 elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
             }
+            is RichMessage -> elements.add(
+                ImMsgBody.Elem(
+                    richMsg = ImMsgBody.RichMsg(
+                        serviceId = it.serviceId,
+                        template1 = byteArrayOf(1) + MiraiPlatformUtils.zip(it.content.toByteArray())
+                    )
+                )
+            )
             is OfflineGroupImage -> elements.add(ImMsgBody.Elem(customFace = it.toJceData()))
             is OnlineGroupImageImpl -> elements.add(ImMsgBody.Elem(customFace = it.delegate))
             is OnlineFriendImageImpl -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
@@ -400,6 +404,28 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
                     }
                 }
             }
+            it.richMsg != null -> {
+                when (it.richMsg.serviceId) {
+                    60 -> message.add(
+                        XMLMessage(
+                            content = MiraiPlatformUtils.unzip(it.richMsg.template1, 1).encodeToString()
+                        )
+                    )
+                    else -> {
+                        @Suppress("DEPRECATION")
+                        MiraiLogger.debug {
+                            "unknown richMsg.serviceId: ${it.richMsg.serviceId}, content=${it.richMsg.template1.contentToString()}, \ntryUnzip=${
+                            kotlin.runCatching {
+                                MiraiPlatformUtils.unzip(it.richMsg.template1, 1).encodeToString()
+                            }.getOrElse { "<failed>" }
+                            }"
+                        }
+                    }
+                }
+            }
+            else -> {
+                println(it._miraiContentToString())
+            }
         }
     }
 
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
index 8cf12fb40..3abcf94ee 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
@@ -191,8 +191,9 @@ internal open class QQAndroidClient(
     lateinit var t104: ByteArray
 }
 
+@OptIn(MiraiInternalAPI::class)
 internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
-    md5(getRandomByteArray(16) + guid)
+    MiraiPlatformUtils.md5(getRandomByteArray(16) + guid)
 
 
 internal class ReserveUinInfo(
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt
index b14d0627a..64c570031 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt
@@ -26,6 +26,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.io.*
 import kotlinx.serialization.InternalSerializationApi
+import net.mamoe.mirai.utils.MiraiPlatformUtils
 
 @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
 internal fun createImageDataPacketSequence( // RequestDataTrans
@@ -77,7 +78,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
                     dataoffset = offset,
                     filesize = dataSize.toLong(),
                     serviceticket = uKey,
-                    md5 = net.mamoe.mirai.utils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize),
+                    md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize),
                     fileMd5 = fileMd5,
                     flag = 0,
                     rtcode = 0
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
index e260813a4..dfb85b6b3 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
@@ -342,7 +342,7 @@ internal object KnownPacketFactories {
             1 -> {
                 input.discardExact(4)
                 input.useBytes { data, length ->
-                    data.unzip(length = length).let {
+                    MiraiPlatformUtils.unzip(data, 0, length).let {
                         val size = it.toInt()
                         if (size == it.size || size == it.size + 4) {
                             it.toReadPacket(offset = 4)
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt
index 7f5d44ac1..e527fe5ce 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt
@@ -15,9 +15,10 @@ import kotlinx.io.core.toByteArray
 import kotlinx.io.core.writeFully
 import net.mamoe.mirai.qqandroid.network.protocol.LoginType
 import net.mamoe.mirai.qqandroid.utils.NetworkType
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.MiraiPlatformUtils
 import net.mamoe.mirai.utils.currentTimeMillis
 import net.mamoe.mirai.utils.io.*
-import net.mamoe.mirai.utils.md5
 import kotlin.random.Random
 
 /**
@@ -76,6 +77,7 @@ fun BytePacketBuilder.t18(
     } shouldEqualsTo 22
 }
 
+@OptIn(MiraiInternalAPI::class)
 fun BytePacketBuilder.t106(
     appId: Long = 16L,
     subAppId: Long = 537062845L,
@@ -96,7 +98,7 @@ fun BytePacketBuilder.t106(
     guid?.requireSize(16)
 
     writeShortLVPacket {
-        encryptAndWrite(md5(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt().toByteArray())) {
+        encryptAndWrite(MiraiPlatformUtils.md5(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt().toByteArray())) {
             writeShort(4)//TGTGTVer
             writeInt(Random.nextInt())
             writeInt(5)//ssoVer
@@ -321,12 +323,13 @@ fun BytePacketBuilder.t144(
     }
 }
 
+@OptIn(MiraiInternalAPI::class)
 fun BytePacketBuilder.t109(
     androidId: ByteArray
 ) {
     writeShort(0x109)
     writeShortLVPacket {
-        writeFully(md5(androidId))
+        writeFully(MiraiPlatformUtils.md5(androidId))
     } shouldEqualsTo 16
 }
 
@@ -556,21 +559,23 @@ fun BytePacketBuilder.t400(
     }
 }
 
+@OptIn(MiraiInternalAPI::class)
 fun BytePacketBuilder.t187(
     macAddress: ByteArray
 ) {
     writeShort(0x187)
     writeShortLVPacket {
-        writeFully(md5(macAddress)) // may be md5
+        writeFully(MiraiPlatformUtils.md5(macAddress)) // may be md5
     }
 }
 
+@OptIn(MiraiInternalAPI::class)
 fun BytePacketBuilder.t188(
     androidId: ByteArray
 ) {
     writeShort(0x188)
     writeShortLVPacket {
-        writeFully(md5(androidId))
+        writeFully(MiraiPlatformUtils.md5(androidId))
     } shouldEqualsTo 16
 }
 
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt
index 67c21ca29..d4f65ae25 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt
@@ -23,9 +23,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.writeSsoPacket
 import net.mamoe.mirai.qqandroid.utils.NetworkType
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.MiraiPlatformUtils
 import net.mamoe.mirai.utils.io.encodeToString
 import net.mamoe.mirai.utils.io.toReadPacket
-import net.mamoe.mirai.utils.localIpAddress
 
 @Suppress("EnumEntryName")
 internal enum class RegPushReason {
@@ -89,6 +90,7 @@ internal class StatSvc {
 
         private const val subAppId = 537062845L
 
+        @OptIn(MiraiInternalAPI::class)
         operator fun invoke(
             client: QQAndroidClient,
             regPushReason: RegPushReason = RegPushReason.appRegister
@@ -138,7 +140,7 @@ internal class StatSvc {
                                 strOSVer = client.device.version.release.encodeToString(),
 
                                 uOldSSOIp = 0,
-                                uNewSSOIp = localIpAddress().split(".").foldIndexed(0L) { index: Int, acc: Long, s: String ->
+                                uNewSSOIp = MiraiPlatformUtils.localIpAddress().split(".").foldIndexed(0L) { index: Int, acc: Long, s: String ->
                                     acc or ((s.toLong() shl (index * 16)))
                                 },
                                 strVendorName = "MIUI",
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
index a08646b24..5b7365394 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
@@ -84,7 +84,7 @@ internal class WtLogin {
                         t8(2052)
                         t104(client.t104)
                         t116(150470524, 66560)
-                        t401(md5(client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402))
+                        t401(MiraiPlatformUtils.md5(client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402))
                     }
                 }
             }
diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/ExternalImageAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/ExternalImageAndroid.kt
index 71d3764e2..2f433f224 100644
--- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/ExternalImageAndroid.kt
+++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/ExternalImageAndroid.kt
@@ -44,6 +44,7 @@ fun Bitmap.toExternalImage(formatName: String = "gif"): ExternalImage {
 /**
  * 读取文件头识别图片属性, 然后构造 [ExternalImage]
  */
+@OptIn(MiraiInternalAPI::class)
 @Throws(IOException::class)
 fun File.toExternalImage(): ExternalImage {
     val input = BitmapFactory.decodeFile(this.absolutePath)
@@ -52,7 +53,7 @@ fun File.toExternalImage(): ExternalImage {
     return ExternalImage(
         width = input.width,
         height = input.height,
-        md5 = this.inputStream().use { it.md5() },
+        md5 = this.inputStream().use { MiraiPlatformUtils.md5(it) },
         imageFormat = this.nameWithoutExtension,
         input = this.inputStream().asInput(IoBuffer.Pool),
         inputSize = this.length(),
diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt
index 7cc35df53..3466ec9b6 100644
--- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt
+++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt
@@ -21,6 +21,8 @@ import kotlinx.serialization.Transient
 import kotlinx.serialization.UnstableDefault
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.JsonConfiguration
+import net.mamoe.mirai.utils.MiraiPlatformUtils.localIpAddress
+import net.mamoe.mirai.utils.MiraiPlatformUtils.md5
 import java.io.File
 
 /**
@@ -102,6 +104,7 @@ actual open class SystemDeviceInfo actual constructor() : DeviceInfo() {
             (context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).connectionInfo.ssid.toByteArray()
         }.getOrElse { byteArrayOf() }
 
+    @OptIn(MiraiInternalAPI::class)
     override val imsiMd5: ByteArray
         @SuppressLint("HardwareIds")
         get() = md5(kotlin.runCatching {
@@ -117,6 +120,8 @@ actual open class SystemDeviceInfo actual constructor() : DeviceInfo() {
                 (context.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager).deviceId
             }
         }.getOrElse { "" }
+
+    @OptIn(MiraiInternalAPI::class)
     override val ipAddress: ByteArray get() = localIpAddress().split(".").map { it.toByte() }.takeIf { it.size == 4 }?.toByteArray() ?: byteArrayOf()
     override val androidId: ByteArray get() = Build.ID.toByteArray()
     override val apn: ByteArray get() = "wifi".toByteArray()
diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt
index 5fe233778..c5b1df78a 100644
--- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt
+++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt
@@ -10,7 +10,8 @@
 package net.mamoe.mirai.utils.cryptor
 
 import android.annotation.SuppressLint
-import net.mamoe.mirai.utils.md5
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.MiraiPlatformUtils.md5
 import java.security.*
 import java.security.spec.ECGenParameterSpec
 import java.security.spec.X509EncodedKeySpec
@@ -71,6 +72,7 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
                 .genKeyPair())
         }
 
+        @OptIn(MiraiInternalAPI::class)
         actual fun calculateShareKey(
             privateKey: ECDHPrivateKey,
             publicKey: ECDHPublicKey
diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt
index 8cb421f09..e8a1157dd 100644
--- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt
+++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt
@@ -7,6 +7,8 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
+@file:Suppress("NOTHING_TO_INLINE")
+
 package net.mamoe.mirai.utils
 
 import io.ktor.client.HttpClient
@@ -15,88 +17,93 @@ import io.ktor.util.KtorExperimentalAPI
 import kotlinx.io.pool.useInstance
 import net.mamoe.mirai.utils.io.ByteArrayPool
 import java.io.ByteArrayOutputStream
-import java.io.DataInput
-import java.io.EOFException
 import java.io.InputStream
 import java.net.InetAddress
 import java.security.MessageDigest
+import java.util.zip.Deflater
 import java.util.zip.Inflater
 
 
-/**
- * Ktor HttpClient. 不同平台使用不同引擎.
- */
-@OptIn(KtorExperimentalAPI::class)
-actual val Http: HttpClient
-    get() = HttpClient(CIO)
-
-/**
- * Localhost 解析
- */
-actual fun localIpAddress(): String = runCatching {
-    InetAddress.getLocalHost().hostAddress
-}.getOrElse { "192.168.1.123" }
-
-/**
- * MD5 算法
- *
- * @return 16 bytes
- */
-actual fun md5(byteArray: ByteArray, offset: Int, length: Int): ByteArray =
-    MessageDigest.getInstance("MD5").apply { update(byteArray, offset, length) }.digest()
-
-fun InputStream.md5(): ByteArray {
-    val digest = MessageDigest.getInstance("md5")
-    digest.reset()
-    this.readInSequence {
-        digest.update(it.toByte())
-    }
-    return digest.digest()
-}
-
-fun DataInput.md5(): ByteArray {
-    val digest = MessageDigest.getInstance("md5")
-    digest.reset()
-    val buffer = byteArrayOf(1)
-    while (true) {
-        try {
-            this.readFully(buffer)
-        } catch (e: EOFException) {
-            break
-        }
-        digest.update(buffer[0])
-    }
-    return digest.digest()
-}
-
-private inline fun InputStream.readInSequence(block: (Int) -> Unit) {
-    var read: Int
-    while (this.read().also { read = it } != -1) {
-        block(read)
-    }
-}
-
-@OptIn(MiraiInternalAPI::class)
-actual fun ByteArray.unzip(offset: Int, length: Int): ByteArray {
-    this.checkOffsetAndLength(offset, length)
-    if (length == 0) return ByteArray(0)
-
-    val inflater = Inflater()
-    inflater.reset()
-    ByteArrayOutputStream().use { output ->
-        inflater.setInput(this, offset, length)
-        ByteArrayPool.useInstance {
-            while (!inflater.finished()) {
-                output.write(it, 0, inflater.inflate(it))
-            }
-        }
-
-        inflater.end()
-        return output.toByteArray()
-    }
-}
-
 /**
  * 时间戳
  */
-actual val currentTimeMillis: Long get() = System.currentTimeMillis()
\ No newline at end of file
+actual val currentTimeMillis: Long get() = System.currentTimeMillis()
+
+@MiraiInternalAPI
+actual object MiraiPlatformUtils {
+    actual fun unzip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        data.checkOffsetAndLength(offset, length)
+        if (length == 0) return ByteArray(0)
+
+        val inflater = Inflater()
+        inflater.reset()
+        ByteArrayOutputStream().use { output ->
+            inflater.setInput(data, offset, length)
+            ByteArrayPool.useInstance {
+                while (!inflater.finished()) {
+                    output.write(it, 0, inflater.inflate(it))
+                }
+            }
+
+            inflater.end()
+            return output.toByteArray()
+        }
+    }
+
+    actual fun zip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        data.checkOffsetAndLength(offset, length)
+        if (length == 0) return ByteArray(0)
+
+        val inflater = Deflater()
+        inflater.reset()
+        ByteArrayOutputStream().use { output ->
+            inflater.setInput(data, offset, length)
+            ByteArrayPool.useInstance {
+                while (!inflater.finished()) {
+                    output.write(it, 0, inflater.deflate(it))
+                }
+            }
+
+            inflater.end()
+            return output.toByteArray()
+        }
+    }
+
+    actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray {
+        data.checkOffsetAndLength(offset, length)
+        return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()
+    }
+
+    actual inline fun md5(str: String): ByteArray = md5(str.toByteArray())
+
+    /**
+     * Ktor HttpClient. 不同平台使用不同引擎.
+     */
+    @OptIn(KtorExperimentalAPI::class)
+    actual val Http: HttpClient
+        get() = HttpClient(CIO)
+
+    /**
+     * Localhost 解析
+     */
+    actual fun localIpAddress(): String = runCatching {
+        InetAddress.getLocalHost().hostAddress
+    }.getOrElse { "192.168.1.123" }
+
+    fun md5(stream: InputStream): ByteArray {
+        val digest = MessageDigest.getInstance("md5")
+        digest.reset()
+        stream.readInSequence {
+            digest.update(it.toByte())
+        }
+        return digest.digest()
+    }
+
+
+    private inline fun InputStream.readInSequence(block: (Int) -> Unit) {
+        var read: Int
+        while (this.read().also { read = it } != -1) {
+            block(read)
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotAccount.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotAccount.kt
index 01968d154..c8a87b302 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotAccount.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotAccount.kt
@@ -14,7 +14,7 @@ package net.mamoe.mirai
 import kotlinx.io.core.toByteArray
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
 import net.mamoe.mirai.utils.MiraiInternalAPI
-import net.mamoe.mirai.utils.md5
+import net.mamoe.mirai.utils.MiraiPlatformUtils
 import kotlin.annotation.AnnotationTarget.*
 
 @MiraiInternalAPI
@@ -28,7 +28,7 @@ data class BotAccount(
     @MiraiInternalAPI
     val passwordMd5: ByteArray // md5
 ) {
-    constructor(id: Long, passwordPlainText: String) : this(id, md5(passwordPlainText.toByteArray()))
+    constructor(id: Long, passwordPlainText: String) : this(id, MiraiPlatformUtils.md5(passwordPlainText.toByteArray()))
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/DeviceInfo.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/DeviceInfo.kt
index b44ed6cbb..2000b3889 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/DeviceInfo.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/DeviceInfo.kt
@@ -94,6 +94,7 @@ abstract class DeviceInfo {
     }
 }
 
+@OptIn(MiraiInternalAPI::class)
 @Serializable
 class DeviceInfoData(
     override val display: ByteArray,
@@ -122,7 +123,8 @@ class DeviceInfoData(
 
     @OptIn(ExperimentalUnsignedTypes::class)
     override val ipAddress: ByteArray
-        get() = localIpAddress().split(".").map { it.toUByte().toByte() }.takeIf { it.size == 4 }?.toByteArray()
+        get() = MiraiPlatformUtils.localIpAddress().split(".").map { it.toUByte().toByte() }.takeIf { it.size == 4 }
+            ?.toByteArray()
             ?: byteArrayOf()
     override val androidId: ByteArray get() = display
 
@@ -138,7 +140,9 @@ class DeviceInfoData(
 /**
  * Defaults "%4;7t>;28<fc.5*6".toByteArray()
  */
-fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray = md5(androidId + macAddress)
+@OptIn(MiraiInternalAPI::class)
+fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
+    MiraiPlatformUtils.md5(androidId + macAddress)
 
 /*
 fun DeviceInfo.toOidb0x769DeviceInfo() : Oidb0x769.DeviceInfo = Oidb0x769.DeviceInfo(
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt
index c4b730d91..fd5f8a083 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt
@@ -12,7 +12,6 @@
 package net.mamoe.mirai.utils
 
 import io.ktor.client.HttpClient
-import kotlinx.io.core.toByteArray
 
 /**
  * 时间戳
@@ -21,30 +20,30 @@ expect val currentTimeMillis: Long
 
 inline val currentTimeSeconds: Long get() = currentTimeMillis / 1000
 
-
 /**
- * 解 zip 压缩
+ * 仅供内部使用的工具类.
+ * 不写为扩展是为了避免污染命名空间.
  */
-expect fun ByteArray.unzip(offset: Int = 0, length: Int = this.size - offset): ByteArray
+@MiraiInternalAPI
+expect object MiraiPlatformUtils {
+    fun unzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
 
-/**
- * MD5 算法
- *
- * @return 16 bytes
- */
-expect fun md5(byteArray: ByteArray, offset: Int = 0, length: Int = byteArray.size - offset): ByteArray
+    fun zip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
 
-inline fun md5(str: String): ByteArray = md5(str.toByteArray())
 
-/**
- * Localhost 解析
- */
-expect fun localIpAddress(): String
+    fun md5(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
+
+    inline fun md5(str: String): ByteArray
+
+    fun localIpAddress(): String
+
+    /**
+     * Ktor HttpClient. 不同平台使用不同引擎.
+     */
+    @MiraiInternalAPI
+    val Http: HttpClient
+}
 
-/**
- * Ktor HttpClient. 不同平台使用不同引擎.
- */
-expect val Http: HttpClient
 
 @Suppress("DuplicatedCode") // false positive. `this` is not the same for `List<Byte>` and `ByteArray`
 internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) {
diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt
index 20a53c769..47bfd47db 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt
@@ -11,8 +11,8 @@
 
 package net.mamoe.mirai.utils
 
-import kotlinx.coroutines.io.ByteReadChannel
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.io.ByteReadChannel
 import kotlinx.coroutines.withContext
 import kotlinx.io.core.Input
 import kotlinx.io.core.buildPacket
@@ -60,6 +60,7 @@ suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withC
 /**
  * 读取文件头识别图片属性, 然后构造 [ExternalImage]
  */
+@OptIn(MiraiInternalAPI::class)
 @Throws(IOException::class)
 fun File.toExternalImage(): ExternalImage {
     val input = ImageIO.createImageInputStream(this)
@@ -71,7 +72,7 @@ fun File.toExternalImage(): ExternalImage {
     return ExternalImage(
         width = image.getWidth(0),
         height = image.getHeight(0),
-        md5 = this.inputStream().md5(), // dont change
+        md5 = MiraiPlatformUtils.md5(this.inputStream()), // dont change
         imageFormat = image.formatName,
         input = this.inputStream(),
         filename = this.name
diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt
index 494cfbe05..1b316fe72 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt
@@ -7,78 +7,103 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:Suppress("EXPERIMENTAL_API_USAGE")
+@file:Suppress("EXPERIMENTAL_API_USAGE", "NOTHING_TO_INLINE")
 
 package net.mamoe.mirai.utils
 
 import io.ktor.client.HttpClient
 import io.ktor.client.engine.cio.CIO
+import io.ktor.util.KtorExperimentalAPI
 import kotlinx.io.pool.useInstance
 import net.mamoe.mirai.utils.io.ByteArrayPool
-import java.io.*
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
 import java.net.InetAddress
 import java.security.MessageDigest
+import java.util.zip.Deflater
 import java.util.zip.Inflater
 
-actual fun md5(byteArray: ByteArray, offset: Int, length: Int): ByteArray =
-    MessageDigest.getInstance("MD5").apply { update(byteArray, offset, length) }.digest()
-
-fun InputStream.md5(): ByteArray = this.use {
-    val digest = MessageDigest.getInstance("md5")
-    digest.reset()
-    this.use { input ->
-        object : OutputStream() {
-            override fun write(b: Int) {
-                digest.update(b.toByte())
-            }
-        }.use { output ->
-            input.copyTo(output)
-        }
-    }
-    return digest.digest()
-}
-
-fun DataInput.md5(): ByteArray {
-    val digest = MessageDigest.getInstance("md5")
-    digest.reset()
-    val buffer = byteArrayOf(1)
-    while (true) {
-        try {
-            this.readFully(buffer)
-        } catch (e: EOFException) {
-            break
-        }
-        digest.update(buffer[0])
-    }
-    return digest.digest()
-}
-
-actual fun localIpAddress(): String = InetAddress.getLocalHost().hostAddress
-
-actual val Http: HttpClient get() = HttpClient(CIO)
-
-@OptIn(MiraiInternalAPI::class)
-actual fun ByteArray.unzip(offset: Int, length: Int): ByteArray {
-    this.checkOffsetAndLength(offset, length)
-    if (length == 0) return ByteArray(0)
-
-    val inflater = Inflater()
-    inflater.reset()
-    ByteArrayOutputStream().use { output ->
-        inflater.setInput(this, offset, length)
-        ByteArrayPool.useInstance {
-            while (!inflater.finished()) {
-                output.write(it, 0, inflater.inflate(it))
-            }
-        }
-
-        inflater.end()
-        return output.toByteArray()
-    }
-}
-
 /**
  * 时间戳
  */
 actual val currentTimeMillis: Long
-    get() = System.currentTimeMillis()
\ No newline at end of file
+    get() = System.currentTimeMillis()
+
+@MiraiInternalAPI
+actual object MiraiPlatformUtils {
+    actual fun unzip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        data.checkOffsetAndLength(offset, length)
+        if (length == 0) return ByteArray(0)
+
+        val inflater = Inflater()
+        inflater.reset()
+        ByteArrayOutputStream().use { output ->
+            inflater.setInput(data, offset, length)
+            ByteArrayPool.useInstance {
+                while (!inflater.finished()) {
+                    output.write(it, 0, inflater.inflate(it))
+                }
+            }
+
+            inflater.end()
+            return output.toByteArray()
+        }
+    }
+
+    actual fun zip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        data.checkOffsetAndLength(offset, length)
+        if (length == 0) return ByteArray(0)
+
+        val inflater = Deflater()
+        inflater.reset()
+        ByteArrayOutputStream().use { output ->
+            inflater.setInput(data, offset, length)
+            ByteArrayPool.useInstance {
+                while (!inflater.finished()) {
+                    output.write(it, 0, inflater.deflate(it))
+                }
+            }
+
+            inflater.end()
+            return output.toByteArray()
+        }
+    }
+
+    actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray {
+        data.checkOffsetAndLength(offset, length)
+        return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()
+    }
+
+    actual inline fun md5(str: String): ByteArray = md5(str.toByteArray())
+
+    /**
+     * Ktor HttpClient. 不同平台使用不同引擎.
+     */
+    @OptIn(KtorExperimentalAPI::class)
+    actual val Http: HttpClient
+        get() = HttpClient(CIO)
+
+    /**
+     * Localhost 解析
+     */
+    actual fun localIpAddress(): String = runCatching {
+        InetAddress.getLocalHost().hostAddress
+    }.getOrElse { "192.168.1.123" }
+
+    fun md5(stream: InputStream): ByteArray {
+        val digest = MessageDigest.getInstance("md5")
+        digest.reset()
+        stream.use { input ->
+            object : OutputStream() {
+                override fun write(b: Int) {
+                    digest.update(b.toByte())
+                }
+            }.use { output ->
+                input.copyTo(output)
+            }
+        }
+        return digest.digest()
+    }
+
+}
\ No newline at end of file
diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt
index efb5cfd47..c9c6729aa 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/SystemDeviceInfo.kt
@@ -15,6 +15,8 @@ import kotlinx.serialization.Transient
 import kotlinx.serialization.UnstableDefault
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.JsonConfiguration
+import net.mamoe.mirai.utils.MiraiPlatformUtils.localIpAddress
+import net.mamoe.mirai.utils.MiraiPlatformUtils.md5
 import net.mamoe.mirai.utils.io.getRandomByteArray
 import net.mamoe.mirai.utils.io.getRandomString
 import java.io.File
@@ -37,7 +39,7 @@ fun File.loadAsDeviceInfo(context: Context = ContextImpl()): DeviceInfo {
 private val JSON = Json(JsonConfiguration.Stable)
 
 @Serializable
-@OptIn(ExperimentalUnsignedTypes::class)
+@OptIn(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
 actual open class SystemDeviceInfo actual constructor() : DeviceInfo() {
     actual constructor(context: Context) : this() {
         this.context = context
diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt
index 67ac25325..3db9de92b 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt
@@ -9,7 +9,8 @@
 
 package net.mamoe.mirai.utils.cryptor
 
-import net.mamoe.mirai.utils.md5
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.MiraiPlatformUtils
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import java.security.*
 import java.security.spec.ECGenParameterSpec
@@ -58,6 +59,7 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
                 .genKeyPair())
         }
 
+        @OptIn(MiraiInternalAPI::class)
         actual fun calculateShareKey(
             privateKey: ECDHPrivateKey,
             publicKey: ECDHPublicKey
@@ -65,7 +67,7 @@ actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
             val instance = KeyAgreement.getInstance("ECDH", "BC")
             instance.init(privateKey)
             instance.doPhase(publicKey, true)
-            return md5(instance.generateSecret())
+            return MiraiPlatformUtils.md5(instance.generateSecret())
         }
 
         actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {