diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5db46b72f..c5e69ff72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 开发版本. 频繁更新, 不保证高稳定性
 
+## `0.27.0` 2020/3/8
+- 支持 `XML`, `Json`, `LightApp` 等 `RichMessage`
+
 ## `0.26.2` 2020/3/8
 - 新增 `MessageChain.repeat` 与 `MessageChain.times`
 - JVM 平台下 `PlatformLogger` 可重定向输出
diff --git a/gradle.properties b/gradle.properties
index c50161ef2..0e6d334e3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,7 +1,7 @@
 # style guide
 kotlin.code.style=official
 # config
-miraiVersion=0.26.2
+miraiVersion=0.27.0
 kotlin.incremental.multiplatform=true
 kotlin.parallel.tasks.in.project=true
 # kotlin
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 09a5bb455..c2e0eb573 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,25 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
                 elements.add(ImMsgBody.Elem(text = it.toJceData()))
                 elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
             }
+            is LightApp -> elements.add(
+                ImMsgBody.Elem(
+                    lightApp = ImMsgBody.LightAppElem(
+                        data = byteArrayOf(1) + MiraiPlatformUtils.zip(it.content.toByteArray())
+                    )
+                )
+            )
+            is RichMessage -> elements.add(
+                ImMsgBody.Elem(
+                    richMsg = ImMsgBody.RichMsg(
+                        serviceId = when (it) {
+                            is XmlMessage -> 60
+                            is JsonMessage -> 1
+                            else -> error("unsupported RichMessage")
+                        },
+                        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))
@@ -269,9 +284,10 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
     }
     this.forEach(::transformOneMessage)
 
-    // if(this.any<QuoteReply>()){
-    elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
-    // }
+    if (this.any<RichMessage>()) {
+        // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
+        elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes())))
+    } else elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
 
     return elements
 }
@@ -400,6 +416,23 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
                     }
                 }
             }
+            it.lightApp != null -> {
+                val content = MiraiPlatformUtils.unzip(it.lightApp.data, 1).encodeToString()
+                message.add(LightApp(content))
+            }
+            it.richMsg != null -> {
+                val content = MiraiPlatformUtils.unzip(it.richMsg.template1, 1).encodeToString()
+                when (it.richMsg.serviceId) {
+                    1 -> message.add(JsonMessage(content))
+                    60 -> message.add(XmlMessage(content))
+                    else -> {
+                        @Suppress("DEPRECATION")
+                        MiraiLogger.debug {
+                            "unknown richMsg.serviceId: ${it.richMsg.serviceId}, content=${it.richMsg.template1.contentToString()}, \ntryUnzip=${content}"
+                        }
+                    }
+                }
+            }
         }
     }
 
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..54f7f60ba 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,87 @@ 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 deflater = Deflater()
+        deflater.setInput(data, offset, length)
+        deflater.finish()
+
+        ByteArrayPool.useInstance {
+            return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
+        }
+    }
+
+    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/data/ImageLink.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/ImageLink.kt
deleted file mode 100644
index 3cef0c4a3..000000000
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/ImageLink.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.data
-
-import io.ktor.client.request.get
-import kotlinx.io.core.ByteReadPacket
-import kotlinx.io.core.readBytes
-import net.mamoe.mirai.utils.Http
-
-interface ImageLink {
-    /**
-     * 原图
-     */
-    val original: String
-
-    suspend fun downloadAsByteArray(): ByteArray = download().readBytes()
-
-    suspend fun download(): ByteReadPacket = Http.get(original)
-}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Json.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Json.kt
new file mode 100644
index 000000000..739bd907e
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Json.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.message.data
+
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import net.mamoe.mirai.utils.SinceMirai
+
+/**
+ * Json 消息.
+ *
+ * @see LightApp 一些消息实际上是 [LightApp]
+ */
+@SinceMirai("0.27.0")
+@OptIn(MiraiExperimentalAPI::class)
+class JsonMessage(override val content: String) : RichMessage {
+    companion object Key : Message.Key<JsonMessage>
+
+    // serviceId = 1
+    override fun toString(): String = content
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/LightApp.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/LightApp.kt
new file mode 100644
index 000000000..7c7277d67
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/LightApp.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.message.data
+
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import net.mamoe.mirai.utils.SinceMirai
+
+/**
+ * 小程序分享, 如音乐分享
+ */
+@OptIn(MiraiExperimentalAPI::class)
+@SinceMirai("0.27.0")
+class LightApp constructor(override val content: String) : RichMessage {
+    companion object Key : Message.Key<LightApp>
+
+    override fun toString(): String = content
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
index 8426a0aef..a12e28029 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
@@ -61,7 +61,7 @@ interface MessageChain : Message, Iterable<SingleMessage> {
     fun <M : Message> getOrNull(key: Message.Key<M>): M? = firstOrNull(key)
 
     /**
-     * 遍历每一个有内容的消息, 即 [At], [AtAll], [PlainText], [Image], [Face], [XMLMessage], [QuoteReply].
+     * 遍历每一个有内容的消息, 即 [At], [AtAll], [PlainText], [Image], [Face], [XmlMessage], [QuoteReply].
      * 仅供 `Java` 使用
      */
     @Suppress("FunctionName", "INAPPLICABLE_JVM_NAME")
@@ -73,7 +73,7 @@ interface MessageChain : Message, Iterable<SingleMessage> {
     }
 
     /**
-     * 遍历每一个消息, 即 [MessageSource] [At], [AtAll], [PlainText], [Image], [Face], [XMLMessage], [QuoteReply].
+     * 遍历每一个消息, 即 [MessageSource] [At], [AtAll], [PlainText], [Image], [Face], [XmlMessage], [QuoteReply].
      * 仅供 `Java` 使用
      */
     @Suppress("FunctionName", "INAPPLICABLE_JVM_NAME")
@@ -88,7 +88,7 @@ interface MessageChain : Message, Iterable<SingleMessage> {
 // region accessors
 
 /**
- * 遍历每一个有内容的消息, 即 [At], [AtAll], [PlainText], [Image], [Face], [XMLMessage], [QuoteReply]
+ * 遍历每一个有内容的消息, 即 [At], [AtAll], [PlainText], [Image], [Face], [XmlMessage], [QuoteReply]
  */
 @JvmSynthetic
 inline fun MessageChain.foreachContent(block: (Message) -> Unit) {
@@ -130,6 +130,10 @@ fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key)
     Face -> first<Face>()
     QuoteReply -> first<QuoteReply>()
     MessageSource -> first<MessageSource>()
+    XmlMessage -> first<XmlMessage>()
+    JsonMessage -> first<JsonMessage>()
+    RichMessage -> first<RichMessage>()
+    LightApp -> first<LightApp>()
     else -> null
 } as M?
 
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
new file mode 100644
index 000000000..af967da8e
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.message.data
+
+import net.mamoe.mirai.utils.SinceMirai
+
+/**
+ * XML 消息等富文本消息
+ *
+ * @see XmlMessage
+ * @see JsonMessage
+ * @see LightApp
+ */
+@SinceMirai("0.27.0")
+interface RichMessage : MessageContent {
+    companion object Key : Message.Key<RichMessage>
+
+    val content: String
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/XML.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/XML.kt
index fb01e7c6c..a61c6d535 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/XML.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/XML.kt
@@ -15,6 +15,7 @@
 package net.mamoe.mirai.message.data
 
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import net.mamoe.mirai.utils.SinceMirai
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 
@@ -23,20 +24,25 @@ import kotlin.jvm.JvmName
  *
  * @see buildXMLMessage
  */
-@MiraiExperimentalAPI
-inline class XMLMessage(val stringValue: String) : Message, MessageContent {
-    override fun followedBy(tail: Message): Nothing = error("XMLMessage Message cannot be followed")
-    override fun toString(): String = stringValue
+@SinceMirai("0.27.0")
+@OptIn(MiraiExperimentalAPI::class)
+class XmlMessage constructor(override val content: String) : RichMessage {
+    companion object Key : Message.Key<XmlMessage>
+
+    // override val serviceId: Int get() = 60
+
+    override fun toString(): String = content
 }
 
 /**
  * 构造一条 XML 消息
  */
-@MiraiExperimentalAPI("还未支持")
-inline fun buildXMLMessage(block: @XMLDsl XMLMessageBuilder.() -> Unit): XMLMessage =
-    XMLMessage(XMLMessageBuilder().apply(block).text)
+@SinceMirai("0.27.0")
+@MiraiExperimentalAPI
+inline fun buildXMLMessage(block: @XMLDsl XMLMessageBuilder.() -> Unit): XmlMessage =
+    XmlMessage(XMLMessageBuilder().apply(block).text)
 
-@Suppress("NOTHING_TO_INLINE")
+@SinceMirai("0.27.0")
 @XMLDsl
 class ItemBuilder(
     var bg: Int = 0,
@@ -46,21 +52,20 @@ class ItemBuilder(
     internal val builder: StringBuilder = StringBuilder()
     val text: String get() = "<item bg='$bg' layout='$layout'>$builder</item>"
 
-    inline fun summary(text: String, color: String = "#FFFFFF") {
+    fun summary(text: String, color: String = "#000000") {
         this.builder.append("<summary color='$color'>$text</summary>")
     }
 
-    inline fun title(text: String, size: Int = 18, color: String = "#FFFFFF") {
+    fun title(text: String, size: Int = 25, color: String = "#000000") {
         this.builder.append("<title size='$size' color='$color'>$text</title>")
     }
 
-    inline fun picture(coverUrl: String) {
+    fun picture(coverUrl: String) {
         this.builder.append("<picture cover='$coverUrl'/>")
     }
 }
 
 @XMLDsl
-@Suppress("NOTHING_TO_INLINE")
 class XMLMessageBuilder(
     var templateId: Int = 1,
     var serviceId: Int = 1,
@@ -70,7 +75,7 @@ class XMLMessageBuilder(
      */
     var actionData: String = "",
     /**
-     * 摘要
+     * 摘要, 在官方客户端内消息列表中显示
      */
     var brief: String = "",
     var flag: Int = 3,
@@ -89,11 +94,11 @@ class XMLMessageBuilder(
                 "</msg>"
 
     @XMLDsl
-    inline fun item(block: @XMLDsl ItemBuilder.() -> Unit) {
+    fun item(block: @XMLDsl ItemBuilder.() -> Unit) {
         builder.append(ItemBuilder().apply(block).text)
     }
 
-    inline fun source(name: String, iconURL: String = "") {
+    fun source(name: String, iconURL: String = "") {
         sourceName = name
         sourceIconURL = iconURL
     }
@@ -101,4 +106,4 @@ class XMLMessageBuilder(
 
 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
 @DslMarker
-internal annotation class XMLDsl
\ No newline at end of file
+annotation class XMLDsl
\ No newline at end of file
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/annotataions.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/annotataions.kt
index 60411fdb3..aa6535a7f 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/annotataions.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/annotataions.kt
@@ -17,7 +17,7 @@ import kotlin.annotation.AnnotationTarget.*
  * 这些 API 可能会在任意时刻更改, 且不会发布任何预警.
  * 非常不建议在发行版本中使用这些 API.
  */
-@Retention(AnnotationRetention.BINARY)
+@Retention(AnnotationRetention.SOURCE)
 @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
 @Target(
     CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR,
@@ -35,7 +35,7 @@ annotation class MiraiInternalAPI(
  * 这些 API 不具有稳定性, 且可能会在任意时刻更改.
  * 不建议在发行版本中使用这些 API.
  */
-@Retention(AnnotationRetention.BINARY)
+@Retention(AnnotationRetention.SOURCE)
 @RequiresOptIn(level = RequiresOptIn.Level.WARNING)
 @Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
 annotation class MiraiExperimentalAPI(
@@ -48,7 +48,7 @@ annotation class MiraiExperimentalAPI(
  * 这些 API 不具有稳定性, 可能会在任意时刻更改, 并且效率非常低下.
  * 非常不建议在发行版本中使用这些 API.
  */
-@Retention(AnnotationRetention.BINARY)
+@Retention(AnnotationRetention.SOURCE)
 @RequiresOptIn(level = RequiresOptIn.Level.WARNING)
 @Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
 annotation class MiraiDebugAPI(
@@ -59,6 +59,6 @@ annotation class MiraiDebugAPI(
  * 标记一个自 Mirai 某个版本起才支持的 API.
  */
 @Target(CLASS, PROPERTY, FIELD, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS)
-@Retention(AnnotationRetention.BINARY)
+@Retention(AnnotationRetention.SOURCE)
 @MustBeDocumented
 annotation class SinceMirai(val version: String)
\ No newline at end of file
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/commonTest/kotlin/net.mamoe.mirai.utils/PlatformUtilsTest.kt b/mirai-core/src/commonTest/kotlin/net.mamoe.mirai.utils/PlatformUtilsTest.kt
new file mode 100644
index 000000000..9bf4d397b
--- /dev/null
+++ b/mirai-core/src/commonTest/kotlin/net.mamoe.mirai.utils/PlatformUtilsTest.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import kotlinx.io.core.toByteArray
+import net.mamoe.mirai.utils.io.encodeToString
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+internal class PlatformUtilsTest {
+
+    @OptIn(MiraiInternalAPI::class)
+    @Test
+    fun testZip() {
+        assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString())
+    }
+}
\ No newline at end of file
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/MiraiLoggerJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/MiraiLoggerJvm.kt
index 7bed2b5b8..17d60ee0d 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/MiraiLoggerJvm.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/MiraiLoggerJvm.kt
@@ -15,7 +15,7 @@ import java.util.*
 /**
  * JVM 控制台日志实现
  */
-actual open class PlatformLogger @JvmOverloads constructor(
+actual open class PlatformLogger constructor(
     override val identity: String? = "Mirai",
     open val output: (String) -> Unit
 ) : MiraiLoggerPlatformBase() {
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..e40057fcd 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,97 @@
  * 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 deflater = Deflater()
+        deflater.setInput(data, offset, length)
+        deflater.finish()
+
+        ByteArrayPool.useInstance {
+            return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
+        }
+    }
+
+    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 {