Browse Source

ExternalResource (#754)

* ExternalResource fundamentals:
- Introduce ExternalResource
- Migrate functions
- Move utilities to mirai-core-utils

* Fix build

* Fix filename and misc improvements

* Close file on ExternalResource.close;
Reset filePointer to 0 on stream close

* Rearrange image extensions

* Fix tests

* Fix build

* toExternalResource: formatName = null by default

* Reduce unnecessary continuations

* Fix ExternalResourceImplByFileWithMd5.inputStream

* ExternalResource: Remove BufferedImage support

* Don't close stream on image upload;
Unified closing behaviorImprove;
Improve FileCacheStrategy;

* Fix createImageDataPacketSequence closing

* Fix image upload, change size to long

* Fix docs

* Rename SendImageUtilsJvmKt to SendResourceUtilsJvmKt

* Run BIO appropriately

* Postpone file detection on formatName getter

* Fix SendResourceUtilsJvmKt JvmName

Co-authored-by: Karlatemp <[email protected]>
Him188 5 years ago
parent
commit
bfda72e58f
51 changed files with 1001 additions and 1445 deletions
  1. 9 1
      mirai-core-api/src/commonMain/kotlin/IMirai.kt
  2. 34 4
      mirai-core-api/src/commonMain/kotlin/contact/Contact.kt
  3. 0 16
      mirai-core-api/src/commonMain/kotlin/contact/Group.kt
  4. 2 2
      mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt
  5. 3 18
      mirai-core-api/src/commonMain/kotlin/contact/User.kt
  6. 11 25
      mirai-core-api/src/commonMain/kotlin/event/events/message.kt
  7. 0 26
      mirai-core-api/src/commonMain/kotlin/message/data/CustomMessage.kt
  8. 0 5
      mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt
  9. 1 2
      mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt
  10. 0 162
      mirai-core-api/src/commonMain/kotlin/utils/ExternalImage.kt
  11. 0 54
      mirai-core-api/src/commonMain/kotlin/utils/ExternalImageJvm.kt
  12. 272 0
      mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt
  13. 49 160
      mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt
  14. 126 0
      mirai-core-api/src/commonMain/kotlin/utils/SendImageUtilsJvmKt.kt
  15. 0 152
      mirai-core-api/src/commonMain/kotlin/utils/internal/ChunkedFlowSession.kt
  16. 0 65
      mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.jvm.kt
  17. 0 28
      mirai-core-api/src/commonMain/kotlin/utils/internal/ReusableInput.kt
  18. 0 145
      mirai-core-api/src/commonMain/kotlin/utils/internal/asReusableInput.kt
  19. 0 29
      mirai-core-api/src/commonMain/kotlin/utils/internal/md5.jvm.kt
  20. 0 156
      mirai-core-api/src/commonMain/kotlin/utils/sendTo.kt
  21. 8 30
      mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt
  22. 68 0
      mirai-core-utils/src/commonMain/kotlin/Bytes.kt
  23. 9 8
      mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt
  24. 46 0
      mirai-core-utils/src/commonMain/kotlin/Files.kt
  25. 146 0
      mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt
  26. 1 0
      mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ExternalImageTest.kt
  27. 2 4
      mirai-core/src/commonMain/kotlin/BotAccount.kt
  28. 6 8
      mirai-core/src/commonMain/kotlin/MiraiImpl.kt
  29. 17 21
      mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt
  30. 2 2
      mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt
  31. 13 20
      mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
  32. 2 2
      mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt
  33. 5 3
      mirai-core/src/commonMain/kotlin/message/conversions.kt
  34. 17 7
      mirai-core/src/commonMain/kotlin/message/imagesImpl.kt
  35. 2 2
      mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt
  36. 109 29
      mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt
  37. 0 92
      mirai-core/src/commonMain/kotlin/network/highway/highway.kt
  38. 4 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt
  39. 8 6
      mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt
  40. 4 3
      mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt
  41. 1 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt
  42. 7 3
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt
  43. 2 1
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt
  44. 0 18
      mirai-core/src/commonMain/kotlin/utils/ByteArrayPool.kt
  45. 0 114
      mirai-core/src/commonMain/kotlin/utils/MiraiPlatformUtils.kt
  46. 3 6
      mirai-core/src/commonMain/kotlin/utils/byteArrays.kt
  47. 1 2
      mirai-core/src/commonMain/kotlin/utils/crypto/TEA.kt
  48. 1 4
      mirai-core/src/commonMain/kotlin/utils/io/input.kt
  49. 6 2
      mirai-core/src/commonTest/kotlin/PlatformUtilsTest.kt
  50. 2 2
      mirai-core/src/commonTest/kotlin/test/printing.kt
  51. 2 2
      mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt

+ 9 - 1
mirai-core-api/src/commonMain/kotlin/IMirai.kt

@@ -23,6 +23,7 @@ import net.mamoe.mirai.message.action.Nudge
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.message.data.Image.Key.queryUrl
 import net.mamoe.mirai.message.data.MessageSource.Key.recall
+import net.mamoe.mirai.utils.FileCacheStrategy
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.MiraiInternalApi
 
@@ -35,7 +36,7 @@ public val Mirai: IMirai by lazy { findMiraiInstance() }
 /**
  * Mirai API 接口.
  *
- * @see Mirai
+ * @see Mirai 获取实例
  */
 public interface IMirai : LowLevelApiAccessor {
     /**
@@ -47,6 +48,13 @@ public interface IMirai : LowLevelApiAccessor {
     @MiraiExperimentalApi
     public val BotFactory: BotFactory
 
+    /**
+     * Mirai 全局使用的 [FileCacheStrategy].
+     */
+    @Suppress("PropertyName")
+    @MiraiExperimentalApi
+    public var FileCacheStrategy: FileCacheStrategy
+
     /**
      * 使用 groupCode 计算 groupUin. 这两个值仅在 mirai 内部协议区分, 一般人使用时无需在意.
      */

+ 34 - 4
mirai-core-api/src/commonMain/kotlin/contact/Contact.kt

@@ -20,9 +20,9 @@ import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.MessageReceipt.Companion.quote
 import net.mamoe.mirai.message.MessageReceipt.Companion.recall
 import net.mamoe.mirai.message.data.*
-import net.mamoe.mirai.utils.ExternalImage
-import net.mamoe.mirai.utils.OverFileSizeMaxException
-import net.mamoe.mirai.utils.WeakRefProperty
+import net.mamoe.mirai.utils.*
+import java.io.File
+import java.io.InputStream
 
 /**
  * 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], 和 [群][Group].
@@ -70,21 +70,51 @@ public interface Contact : ContactOrBot, CoroutineScope {
     /**
      * 上传一个图片以备发送.
      *
+     * 无论上传是否成功都不会关闭 [resource].
+     *
      * @see Image 查看有关图片的更多信息, 如上传图片
      *
      * @see BeforeImageUploadEvent 图片发送前事件, 可拦截.
      * @see ImageUploadEvent 图片发送完成事件, 不可拦截.
      *
+     * @see ExternalResource
+     *
      * @throws EventCancelledException 当发送消息事件被取消时抛出
      * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, 但 mirai 限制的大小为 30 MB)
      */
     @JvmBlockingBridge
-    public suspend fun uploadImage(image: ExternalImage): Image
+    public suspend fun uploadImage(resource: ExternalResource): Image
 
     /**
      * @return "Friend($id)" or "Group($id)" or "Member($id)"
      */
     public override fun toString(): String
+
+    public companion object {
+        /**
+         * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
+         *
+         * 注意:此函数不会关闭 [imageStream]
+         *
+         * @throws OverFileSizeMaxException
+         * @see FileCacheStrategy
+         */
+        @Throws(OverFileSizeMaxException::class)
+        @JvmStatic
+        @JvmBlockingBridge
+        public suspend fun <C : Contact> C.sendImage(imageStream: InputStream): MessageReceipt<C> =
+            imageStream.sendAsImageTo(this)
+
+        /**
+         * 将文件作为图片发送到指定联系人
+         * @throws OverFileSizeMaxException
+         * @see FileCacheStrategy
+         */
+        @Throws(OverFileSizeMaxException::class)
+        @JvmStatic
+        @JvmBlockingBridge
+        public suspend fun <C : Contact> C.sendImage(file: File): MessageReceipt<C> = file.sendAsImageTo(this)
+    }
 }
 
 /**

+ 0 - 16
mirai-core-api/src/commonMain/kotlin/contact/Group.kt

@@ -18,7 +18,6 @@ import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.MessageReceipt.Companion.recall
 import net.mamoe.mirai.message.data.*
-import net.mamoe.mirai.utils.ExternalImage
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.OverFileSizeMaxException
 import net.mamoe.mirai.utils.PlannedRemoval
@@ -162,21 +161,6 @@ public interface Group : Contact, CoroutineScope {
     public override suspend fun sendMessage(message: String): MessageReceipt<Group> =
         this.sendMessage(message.toPlainText())
 
-
-    /**
-     * 上传一个图片以备发送.
-     *
-     * @see Image 查看有关图片的更多信息, 如上传图片
-     *
-     * @see BeforeImageUploadEvent 图片上传前事件, cancellable
-     * @see ImageUploadEvent 图片上传完成事件
-     *
-     * @throws EventCancelledException 当发送消息事件被取消
-     * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
-     */
-    @JvmBlockingBridge
-    public override suspend fun uploadImage(image: ExternalImage): Image
-
     /**
      * 上传一个语音消息以备发送.
      * 请手动关闭输入流

+ 2 - 2
mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt

@@ -18,7 +18,7 @@ import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD
 import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE
-import net.mamoe.mirai.utils.ExternalImage
+import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.MiraiInternalApi
 
@@ -42,7 +42,7 @@ public interface OtherClient : Contact {
         throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.")
     }
 
-    override suspend fun uploadImage(image: ExternalImage): Image {
+    override suspend fun uploadImage(resource: ExternalResource): Image {
         throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.")
     }
 }

+ 3 - 18
mirai-core-api/src/commonMain/kotlin/contact/User.kt

@@ -14,18 +14,17 @@ package net.mamoe.mirai.contact
 import kotlinx.coroutines.CoroutineScope
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.event.events.*
+import net.mamoe.mirai.event.events.EventCancelledException
+import net.mamoe.mirai.event.events.UserMessagePostSendEvent
+import net.mamoe.mirai.event.events.UserMessagePreSendEvent
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.MessageReceipt.Companion.recall
 import net.mamoe.mirai.message.action.Nudge
 import net.mamoe.mirai.message.action.UserNudge
-import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.isContentEmpty
 import net.mamoe.mirai.message.data.toPlainText
-import net.mamoe.mirai.utils.ExternalImage
 import net.mamoe.mirai.utils.MiraiExperimentalApi
-import net.mamoe.mirai.utils.OverFileSizeMaxException
 
 /**
  * 代表一个 **用户**.
@@ -92,20 +91,6 @@ public interface User : Contact, UserOrBot, CoroutineScope {
      */
     @MiraiExperimentalApi
     public override fun nudge(): UserNudge
-
-    /**
-     * 上传一个图片以备发送.
-     *
-     * @see Image 查看有关图片的更多信息, 如上传图片
-     *
-     * @see BeforeImageUploadEvent 图片发送前事件, cancellable
-     * @see ImageUploadEvent 图片发送完成事件
-     *
-     * @throws EventCancelledException 当发送消息事件被取消
-     * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
-     */
-    @JvmBlockingBridge
-    public override suspend fun uploadImage(image: ExternalImage): Image
 }
 
 /**

+ 11 - 25
mirai-core-api/src/commonMain/kotlin/event/events/message.kt

@@ -16,6 +16,7 @@ package net.mamoe.mirai.event.events
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.*
+import net.mamoe.mirai.contact.Contact.Companion.sendImage
 import net.mamoe.mirai.event.*
 import net.mamoe.mirai.event.events.ImageUploadEvent.Failed
 import net.mamoe.mirai.event.events.ImageUploadEvent.Succeed
@@ -26,7 +27,8 @@ import net.mamoe.mirai.message.data.Image.Key.queryUrl
 import net.mamoe.mirai.message.data.MessageSource.Key.quote
 import net.mamoe.mirai.message.isContextIdenticalWith
 import net.mamoe.mirai.utils.*
-import java.awt.image.BufferedImage
+import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
 import java.io.File
 import java.io.InputStream
 import kotlin.internal.InlineOnly
@@ -415,7 +417,7 @@ public val MessageRecallEvent.isByBot: Boolean
  */
 public data class BeforeImageUploadEvent @MiraiInternalApi constructor(
     public val target: Contact,
-    public val source: ExternalImage
+    public val source: ExternalResource
 ) : BotEvent, BotActiveEvent, AbstractEvent(), CancellableEvent {
     public override val bot: Bot
         get() = target.bot
@@ -434,19 +436,19 @@ public data class BeforeImageUploadEvent @MiraiInternalApi constructor(
  */
 public sealed class ImageUploadEvent : BotEvent, BotActiveEvent, AbstractEvent() {
     public abstract val target: Contact
-    public abstract val source: ExternalImage
+    public abstract val source: ExternalResource
     public override val bot: Bot
         get() = target.bot
 
     public data class Succeed @MiraiInternalApi constructor(
         override val target: Contact,
-        override val source: ExternalImage,
+        override val source: ExternalResource,
         val image: Image
     ) : ImageUploadEvent()
 
     public data class Failed @MiraiInternalApi constructor(
         override val target: Contact,
-        override val source: ExternalImage,
+        override val source: ExternalResource,
         val errno: Int,
         val message: String
     ) : ImageUploadEvent()
@@ -631,9 +633,9 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() {
     public override suspend fun reply(plain: String): MessageReceipt<Contact> =
         subject.sendMessage(PlainText(plain).asMessageChain())
 
-    public override suspend fun ExternalImage.upload(): Image = this.upload(subject)
+    public override suspend fun ExternalResource.uploadAsImage(): Image = this.uploadAsImage(subject)
 
-    public override suspend fun ExternalImage.send(): MessageReceipt<Contact> = this.sendTo(subject)
+    public override suspend fun ExternalResource.sendAsImage(): MessageReceipt<Contact> = this.sendAsImageTo(subject)
 
     public override suspend fun Image.send(): MessageReceipt<Contact> = this.sendTo(subject)
 
@@ -667,25 +669,21 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() {
 
     // region 上传图片
 
-    public override suspend fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image)
     public override suspend fun uploadImage(image: InputStream): Image = subject.uploadImage(image)
     public override suspend fun uploadImage(image: File): Image = subject.uploadImage(image)
     // endregion
 
     // region 发送图片
-    public override suspend fun sendImage(image: BufferedImage): MessageReceipt<Contact> = subject.sendImage(image)
     public override suspend fun sendImage(image: InputStream): MessageReceipt<Contact> = subject.sendImage(image)
     public override suspend fun sendImage(image: File): MessageReceipt<Contact> = subject.sendImage(image)
     // endregion
 
     // region 上传图片 (扩展)
-    public override suspend fun BufferedImage.upload(): Image = upload(subject)
     public override suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject)
     public override suspend fun File.uploadAsImage(): Image = uploadAsImage(subject)
     // endregion 上传图片 (扩展)
 
     // region 发送图片 (扩展)
-    public override suspend fun BufferedImage.send(): MessageReceipt<Contact> = sendTo(subject)
     public override suspend fun InputStream.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
     public override suspend fun File.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
     // endregion 发送图片 (扩展)
@@ -773,10 +771,10 @@ public interface MessageEventExtensions<out TSender : User, out TSubject : Conta
     // endregion
 
     @JvmSynthetic
-    public suspend fun ExternalImage.upload(): Image
+    public suspend fun ExternalResource.uploadAsImage(): Image
 
     @JvmSynthetic
-    public suspend fun ExternalImage.send(): MessageReceipt<TSubject>
+    public suspend fun ExternalResource.sendAsImage(): MessageReceipt<TSubject>
 
     @JvmSynthetic
     public suspend fun Image.send(): MessageReceipt<TSubject>
@@ -828,9 +826,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
 
     // region 上传图片
 
-    @JvmBlockingBridge
-    public suspend fun uploadImage(image: BufferedImage): Image
-
     @JvmBlockingBridge
     public suspend fun uploadImage(image: InputStream): Image
 
@@ -839,9 +834,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
     // endregion
 
     // region 发送图片
-    @JvmBlockingBridge
-    public suspend fun sendImage(image: BufferedImage): MessageReceipt<TSubject>
-
     @JvmBlockingBridge
     public suspend fun sendImage(image: InputStream): MessageReceipt<TSubject>
 
@@ -850,9 +842,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
     // endregion
 
     // region 上传图片 (扩展)
-    @JvmSynthetic
-    public suspend fun BufferedImage.upload(): Image
-
     @JvmSynthetic
     public suspend fun InputStream.uploadAsImage(): Image
 
@@ -861,9 +850,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
     // endregion 上传图片 (扩展)
 
     // region 发送图片 (扩展)
-    @JvmSynthetic
-    public suspend fun BufferedImage.send(): MessageReceipt<TSubject>
-
     @JvmSynthetic
     public suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject>
 

+ 0 - 26
mirai-core-api/src/commonMain/kotlin/message/data/CustomMessage.kt

@@ -19,7 +19,6 @@ import kotlinx.serialization.protobuf.ProtoBuf
 import kotlinx.serialization.protobuf.ProtoNumber
 import net.mamoe.mirai.message.MessageSerializer
 import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.utils.internal.checkOffsetAndLength
 
 /**
  * 自定义消息
@@ -211,28 +210,3 @@ internal inline fun <T : CustomMessageMetadata> T.customToStringImpl(factory: Cu
     @Suppress("UNCHECKED_CAST")
     return (factory as CustomMessage.Factory<T>).dump(this)
 }
-
-@OptIn(ExperimentalUnsignedTypes::class)
-@JvmOverloads
-@Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray
-internal fun ByteArray.toUHexString(
-    separator: String = " ",
-    offset: Int = 0,
-    length: Int = this.size - offset
-): String {
-    this.checkOffsetAndLength(offset, length)
-    if (length == 0) {
-        return ""
-    }
-    val lastIndex = offset + length
-    return buildString(length * 2) {
-        [email protected] { index, it ->
-            if (index in offset until lastIndex) {
-                var ret = it.toUByte().toString(16).toUpperCase()
-                if (ret.length == 1) ret = "0$ret"
-                append(ret)
-                if (index < lastIndex - 1) append(separator)
-            }
-        }
-    }
-}

+ 0 - 5
mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt

@@ -112,10 +112,6 @@ public open class BotConfiguration { // open for Java
      */
     public var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub
 
-    /** 缓存策略  */
-    @MiraiExperimentalApi
-    public var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault
-
     /**
      * Json 序列化器, 使用 'kotlinx.serialization'
      */
@@ -270,7 +266,6 @@ public open class BotConfiguration { // open for Java
             new.reconnectionRetryTimes = reconnectionRetryTimes
             new.loginSolver = loginSolver
             new.protocol = protocol
-            new.fileCacheStrategy = fileCacheStrategy
         }
     }
 

+ 1 - 2
mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt

@@ -17,7 +17,6 @@ import kotlinx.serialization.protobuf.ProtoNumber
 import net.mamoe.mirai.utils.internal.getRandomByteArray
 import net.mamoe.mirai.utils.internal.getRandomIntString
 import net.mamoe.mirai.utils.internal.getRandomString
-import net.mamoe.mirai.utils.internal.md5
 import java.io.File
 
 /**
@@ -78,7 +77,7 @@ public class DeviceInfo(
                 model = "mirai".toByteArray(),
                 bootloader = "unknown".toByteArray(),
                 fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${getRandomIntString(7)}:user/release-keys".toByteArray(),
-                bootId = ExternalImage.generateUUID(getRandomByteArray(16).md5()).toByteArray(),
+                bootId = generateUUID(getRandomByteArray(16).md5()).toByteArray(),
                 procVersion = "Linux version 3.0.31-${getRandomString(8)} ([email protected])".toByteArray(),
                 baseBand = byteArrayOf(),
                 version = Version(),

+ 0 - 162
mirai-core-api/src/commonMain/kotlin/utils/ExternalImage.kt

@@ -1,162 +0,0 @@
-/*
- * Copyright 2019-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
- */
-
-@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
-
-package net.mamoe.mirai.utils
-
-import kotlinx.io.core.readBytes
-import net.mamoe.mirai.contact.Contact
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.User
-import net.mamoe.mirai.message.MessageReceipt
-import net.mamoe.mirai.message.data.Image
-import net.mamoe.mirai.message.data.sendTo
-import net.mamoe.mirai.message.data.toUHexString
-import net.mamoe.mirai.utils.internal.DeferredReusableInput
-import net.mamoe.mirai.utils.internal.ReusableInput
-import java.io.File
-
-/**
- * mirai 将在未来重构 [ExternalImage] 相关 API, 请尽量避免使用他们.
- *
- * 可以直接通过 [File.uploadAsImageTo] 等 API 替代.
- */
-@RequiresOptIn(
-    "mirai 将在 2.0.0 时重构 ExternalImage 相关 API, 请尽量避免使用他们. 可以直接通过 File.uploadAsImageTo() 等 API 替代.",
-    level = RequiresOptIn.Level.WARNING
-)
-@Retention(AnnotationRetention.BINARY)
-@UnstableExternalImage
-public annotation class UnstableExternalImage
-
-/**
- * 外部图片. 图片数据还没有读取到内存.
- *
- * 在 JVM, 请查看 'ExternalImageJvm.kt'
- *
- * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人
- * @See ExternalImage.upload 上传图片并得到 [Image] 消息
- */
-@UnstableExternalImage
-public class ExternalImage internal constructor(
-    internal val input: ReusableInput
-) {
-    internal val md5: ByteArray get() = input.md5
-    public val formatName: String by lazy {
-        val hex = input.asInput().use {
-            it.readBytes(8).toUHexString("")
-        }
-
-        return@lazy hex.detectFormatName()
-    }
-
-    init {
-        if (input !is DeferredReusableInput) {
-            require(input.size < 30L * 1024 * 1024) { "Image file is too big. Maximum is 30 MiB, but recommended to be 20 MiB" }
-        }
-    }
-
-    public companion object {
-        public const val defaultFormatName: String = "mirai"
-
-
-        @MiraiExperimentalApi
-        public fun generateUUID(md5: ByteArray): String {
-            return "${md5[0, 3]}-${md5[4, 5]}-${md5[6, 7]}-${md5[8, 9]}-${md5[10, 15]}"
-        }
-
-        @MiraiExperimentalApi
-        @JvmOverloads
-        public fun generateImageId(md5: ByteArray, format: String = defaultFormatName): String {
-            return """{${generateUUID(md5)}}.$format"""
-        }
-    }
-
-    public override fun toString(): String {
-        if (input is DeferredReusableInput) {
-            if (!input.initialized) {
-                return "ExternalImage(uninitialized)"
-            }
-        }
-        return "ExternalImage(${generateUUID(md5)})"
-    }
-
-    internal fun calculateImageResourceId(): String = generateImageId(md5, formatName)
-
-    private fun String.detectFormatName(): String = when {
-        startsWith("FFD8") -> "jpg"
-        startsWith("89504E47") -> "png"
-        startsWith("47494638") -> "gif"
-        startsWith("424D") -> "bmp"
-        else -> defaultFormatName
-    }
-}
-
-/*
- * ImgType:
- *  JPG:    1000
- *  PNG:    1001
- *  WEBP:   1002
- *  BMP:    1005
- *  GIG:    2000 // gig? gif?
- *  APNG:   2001
- *  SHARPP: 1004
- */
-
-/**
- * 将图片作为单独的消息发送给指定联系人.
- *
- * @see Contact.uploadImage 上传图片
- * @see Contact.sendMessage 最终调用, 发送消息.
- */
-@JvmSynthetic
-public suspend fun <C : Contact> ExternalImage.sendTo(contact: C): MessageReceipt<C> = when (contact) {
-    is Group -> contact.uploadImage(this).sendTo(contact)
-    is User -> contact.uploadImage(this).sendTo(contact)
-    else -> error("unreachable")
-}
-
-/**
- * 上传图片并构造 [Image].
- * 这个函数可能需消耗一段时间.
- *
- * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人
- *
- * @see Contact.uploadImage 最终调用, 上传图片.
- */
-@JvmSynthetic
-public suspend fun ExternalImage.upload(contact: Contact): Image = when (contact) {
-    is Group -> contact.uploadImage(this)
-    is User -> contact.uploadImage(this)
-    else -> error("unreachable")
-}
-
-/**
- * 将图片作为单独的消息发送给 [this]
- *
- * @see Contact.sendMessage 最终调用, 发送消息.
- */
-@JvmSynthetic
-public suspend inline fun <C : Contact> C.sendImage(image: ExternalImage): MessageReceipt<C> = image.sendTo(this)
-
-
-@JvmSynthetic
-internal operator fun ByteArray.get(rangeStart: Int, rangeEnd: Int): String = buildString {
-    for (it in rangeStart..rangeEnd) {
-        append(this@get[it].fixToString())
-    }
-}
-
-private fun Byte.fixToString(): String {
-    return when (val b = this.toInt() and 0xff) {
-        in 0..15 -> "0${this.toString(16).toUpperCase()}"
-        else -> b.toString(16).toUpperCase()
-    }
-}

+ 0 - 54
mirai-core-api/src/commonMain/kotlin/utils/ExternalImageJvm.kt

@@ -1,54 +0,0 @@
-/*
- * Copyright 2019-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
- */
-
-@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
-
-package net.mamoe.mirai.utils
-
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.utils.internal.DeferredReusableInput
-import net.mamoe.mirai.utils.internal.asReusableInput
-import java.awt.image.BufferedImage
-import java.io.File
-import java.io.InputStream
-
-/*
- * 将各类型图片容器转为 [ExternalImage]
- */
-
-
-/**
- * 将 [BufferedImage] 保存为临时文件, 然后构造 [ExternalImage]
- */
-@JvmOverloads
-public fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage =
-    ExternalImage(DeferredReusableInput(this, formatName))
-
-/**
- * 将文件作为 [ExternalImage] 使用. 只会在需要的时候打开文件并读取数据.
- * @param deleteOnClose 若为 `true`, 图片发送后将会删除这个文件
- */
-@JvmOverloads
-public fun File.toExternalImage(deleteOnClose: Boolean = false): ExternalImage {
-    require(this.isFile) { "File must be a file" }
-    require(this.exists()) { "File must exist" }
-    require(this.canRead()) { "File must can be read" }
-    return ExternalImage(asReusableInput(deleteOnClose))
-}
-
-/**
- * 将 [InputStream] 委托为 [ExternalImage].
- * 只会在上传图片时才读取 [InputStream] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy]
- */
-public fun InputStream.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
-
-/**
- * 将 [ByteArray] 委托为 [ExternalImage].
- */
-public fun ByteArray.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))

+ 272 - 0
mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt

@@ -0,0 +1,272 @@
+/*
+ * Copyright 2019-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
+ */
+
+@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
+
+package net.mamoe.mirai.utils
+
+import net.mamoe.kjbb.JvmBlockingBridge
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Contact.Companion.sendImage
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.User
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.Image
+import net.mamoe.mirai.message.data.sendTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.sendImage
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+import java.io.*
+
+
+/**
+ * 一个*不可变的*外部资源.
+ *
+ * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
+ *
+ * ## 创建
+ * - [File.toExternalResource]
+ * - [RandomAccessFile.toExternalResource]
+ * - [ByteArray.toExternalResource]
+ * - [InputStream.toExternalResource]
+ *
+ * ## 释放
+ *
+ * 当 [ExternalResource] 创建时就可能会打开个文件 (如使用 [File.toExternalResource]).
+ * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close].
+ *
+ * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image]
+ * @see ExternalResource.sendAsImageTo 将资源作为图片发送
+ * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image]
+ * @see Contact.sendImage 发送一个资源作为图片
+ *
+ * @see FileCacheStrategy
+ */
+public interface ExternalResource : Closeable {
+
+    /**
+     * 文件内容 MD5. 16 bytes
+     */
+    public val md5: ByteArray
+
+    /**
+     * 文件格式,如 "png", "amr". 当无法自动识别格式时为 "mirai"
+     */
+    public val formatName: String
+
+    /**
+     * 文件大小 bytes
+     */
+    public val size: Long
+
+    /**
+     * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流.
+     *
+     * 关闭此流不会关闭 [ExternalResource].
+     */
+    public fun inputStream(): InputStream
+
+    @MiraiInternalApi
+    public fun calculateResourceId(): String {
+        return generateImageId(md5, formatName.ifEmpty { "mirai" })
+    }
+
+    public companion object {
+        /**
+         * 在无法识别文件格式时使用的默认格式名.
+         *
+         * @see ExternalResource.formatName
+         */
+        public const val DEFAULT_FORMAT_NAME: String = "mirai"
+
+        /**
+         * **打开文件**并创建 [ExternalResource].
+         *
+         * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close].
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        public fun File.toExternalResource(formatName: String? = null): ExternalResource =
+            RandomAccessFile(this, "r").toExternalResource(formatName)
+
+        /**
+         * 创建 [ExternalResource].
+         *
+         * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会.
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        public fun RandomAccessFile.toExternalResource(
+            formatName: String? = null,
+            closeOriginalFileOnClose: Boolean = true
+        ): ExternalResource =
+            ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose)
+
+        /**
+         * 创建 [ExternalResource]
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource =
+            ExternalResourceImplByByteArray(this, formatName)
+
+
+        /**
+         * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource].
+         *
+         * 注意:本函数不会关闭流
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        @Throws(IOException::class)
+        public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource =
+            Mirai.FileCacheStrategy.newCache(this, formatName)
+
+
+        /**
+         * 将图片作为单独的消息发送给指定联系人.
+         *
+         * 注意:本函数不会关闭 [ExternalResource]
+         *
+         *
+         * @see Contact.uploadImage 上传图片
+         * @see Contact.sendMessage 最终调用, 发送消息.
+         */
+        @JvmBlockingBridge
+        @JvmStatic
+        @JvmName("sendAsImage")
+        public suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
+            when (contact) {
+                is Group -> contact.uploadImage(this).sendTo(contact)
+                is User -> contact.uploadImage(this).sendTo(contact)
+                else -> error("unreachable")
+            }
+
+        /**
+         * 上传图片并构造 [Image].
+         * 这个函数可能需消耗一段时间.
+         *
+         * 注意:本函数不会关闭 [ExternalResource]
+         *
+         * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人
+         *
+         * @see Contact.uploadImage 最终调用, 上传图片.
+         */
+        @JvmBlockingBridge
+        @JvmStatic
+        public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = when (contact) {
+            is Group -> contact.uploadImage(this)
+            is User -> contact.uploadImage(this)
+            else -> error("unreachable")
+        }
+
+        /**
+         * 将图片作为单独的消息发送给 [this]
+         *
+         * @see Contact.sendMessage 最终调用, 发送消息.
+         */
+        @JvmSynthetic
+        public suspend inline fun <C : Contact> C.sendImage(image: ExternalResource): MessageReceipt<C> =
+            image.sendAsImageTo(this)
+    }
+}
+
+
+private fun InputStream.detectFileTypeAndClose(): String? {
+    val buffer = ByteArray(8)
+    return use {
+        kotlin.runCatching { it.read(buffer) }.onFailure { return null }
+        getFileType(buffer)
+    }
+}
+
+internal class ExternalResourceImplByFileWithMd5(
+    private val file: RandomAccessFile,
+    override val md5: ByteArray,
+    formatName: String?
+) : ExternalResource {
+    override val size: Long = file.length()
+    override val formatName: String by lazy {
+        formatName ?: inputStream().detectFileTypeAndClose().orEmpty()
+    }
+
+    override fun inputStream(): InputStream {
+        check(file.filePointer == 0L) { "RandomAccessFile.inputStream cannot be opened simultaneously." }
+        return file.inputStream()
+    }
+
+    override fun close() {
+        file.close()
+    }
+}
+
+internal class ExternalResourceImplByFile(
+    private val file: RandomAccessFile,
+    formatName: String?,
+    private val closeOriginalFileOnClose: Boolean = true
+) : ExternalResource {
+    override val size: Long = file.length()
+    override val md5: ByteArray by lazy { inputStream().md5() }
+    override val formatName: String by lazy {
+        formatName ?: inputStream().detectFileTypeAndClose().orEmpty()
+    }
+
+    override fun inputStream(): InputStream {
+        check(file.filePointer == 0L) { "RandomAccessFile.inputStream cannot be opened simultaneously." }
+        return file.inputStream()
+    }
+
+    override fun close() {
+        if (closeOriginalFileOnClose) file.close()
+    }
+}
+
+internal class ExternalResourceImplByByteArray(
+    private val data: ByteArray,
+    formatName: String?
+) : ExternalResource {
+    override val size: Long = data.size.toLong()
+    override val md5: ByteArray by lazy { data.md5() }
+    override val formatName: String by lazy {
+        formatName ?: getFileType(data.copyOf(8)).orEmpty()
+    }
+
+    override fun inputStream(): InputStream = data.inputStream()
+    override fun close() {}
+}
+
+private fun RandomAccessFile.inputStream(): InputStream {
+    val file = this
+    return object : InputStream() {
+        override fun read(): Int = file.read()
+        override fun read(b: ByteArray, off: Int, len: Int): Int = file.read(b, off, len)
+        override fun close() {
+            file.seek(0)
+        }
+        // don't close file on stream.close. stream may be obtained at multiple times.
+    }.buffered()
+}
+
+
+/*
+ * ImgType:
+ *  JPG:    1000
+ *  PNG:    1001
+ *  WEBP:   1002
+ *  BMP:    1005
+ *  GIG:    2000 // gig? gif?
+ *  APNG:   2001
+ *  SHARPP: 1004
+ */

+ 49 - 160
mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt

@@ -9,108 +9,57 @@
 
 package net.mamoe.mirai.utils
 
-import kotlinx.io.core.*
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.utils.internal.asReusableInput
-import java.awt.image.BufferedImage
-import java.io.ByteArrayOutputStream
+import kotlinx.coroutines.Dispatchers
+import net.mamoe.mirai.IMirai
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
 import java.io.File
+import java.io.IOException
 import java.io.InputStream
-import java.io.OutputStream
-import java.net.URL
-import java.security.MessageDigest
-import javax.imageio.ImageIO
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
 
 /**
- * 缓存策略.
+ * 资源缓存策略.
  *
- * 图片上传时默认使用文件缓存.
+ * 由于上传资源时服务器要求提前给出 MD5 和文件大小等数据, 一些资源如 [InputStream] 需要首先缓存才能使用.
  *
- * @see BotConfiguration.fileCacheStrategy 为 [Bot] 指定缓存策略
+ * Mirai 全局都使用 [IMirai.FileCacheStrategy].
+ *
+ * ### 使用 [FileCacheStrategy] 的操作
+ * [ExternalResource.toExternalResource],
+ * [InputStream.uploadAsImage],
+ * [InputStream.sendAsImageTo]
+ *
+ * @see ExternalResource
  */
-@MiraiExperimentalApi
 public interface FileCacheStrategy {
     /**
-     * 将 [input] 缓存为 [ExternalImage].
-     * 此函数应 close 这个 [Input]
-     */
-    @MiraiExperimentalApi
-    @Throws(java.io.IOException::class)
-    public fun newImageCache(input: Input): ExternalImage
-
-    /**
-     * 将 [input] 缓存为 [ExternalImage].
-     * 此函数应 close 这个 [InputStream]
-     */
-    @MiraiExperimentalApi
-    @Throws(java.io.IOException::class)
-    public fun newImageCache(input: InputStream): ExternalImage
-
-    /**
-     * 将 [input] 缓存为 [ExternalImage].
-     * 此 [input] 的内容应是不变的.
-     */
-    @MiraiExperimentalApi
-    @Throws(java.io.IOException::class)
-    public fun newImageCache(input: ByteArray): ExternalImage
-
-    /**
-     * 将 [input] 缓存为 [ExternalImage].
-     * 此 [input] 的内容应是不变的.
-     */
-    @MiraiExperimentalApi
-    @Throws(java.io.IOException::class)
-    public fun newImageCache(input: BufferedImage, format: String = "png"): ExternalImage
-
-    /**
-     * 将 [input] 缓存为 [ExternalImage].
+     * 立即读取 [input] 所有内容并缓存为 [ExternalResource].
+     *
+     * 注意:
+     * - 此函数不会关闭输入
+     * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]).
+     *
+     * @param formatName 文件类型. 此参数通常只会影响官方客户端接收到的文件的文件后缀. 若为 `null` 则会自动根据文件头识别. 识别失败时将使用 "mirai"
      */
-    @MiraiExperimentalApi
-    @Throws(java.io.IOException::class)
-    public fun newImageCache(input: URL): ExternalImage
+    @Throws(IOException::class)
+    public fun newCache(input: InputStream, formatName: String? = null): ExternalResource
 
     /**
-     * 默认的缓存方案, 使用系统临时文件夹存储.
+     * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. 自动根据文件头识别文件类型. 识别失败时将使用 "mirai".
+     *
+     * 注意:
+     * - 此函数不会关闭输入
+     * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]).
      */
-    @MiraiExperimentalApi
-    public object PlatformDefault : FileCacheStrategy by TempCache(null)
+    @Throws(IOException::class)
+    public fun newCache(input: InputStream): ExternalResource = newCache(input, null)
 
     /**
      * 使用内存直接存储所有图片文件.
      */
     public object MemoryCache : FileCacheStrategy {
-        @MiraiExperimentalApi
-        @Throws(java.io.IOException::class)
-        override fun newImageCache(input: Input): ExternalImage {
-            return newImageCache(input.readBytes())
-        }
-
-        @MiraiExperimentalApi
-        @Throws(java.io.IOException::class)
-        override fun newImageCache(input: InputStream): ExternalImage {
-            return newImageCache(input.readBytes())
-        }
-
-        @MiraiExperimentalApi
-        @Throws(java.io.IOException::class)
-        override fun newImageCache(input: ByteArray): ExternalImage {
-            return ExternalImage(input.asReusableInput())
-        }
-
-        @MiraiExperimentalApi
-        override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
-            val out = ByteArrayOutputStream()
-            ImageIO.write(input, format, out)
-            return newImageCache(out.toByteArray())
-        }
-
-        @MiraiExperimentalApi
-        override fun newImageCache(input: URL): ExternalImage {
-            val out = ByteArrayOutputStream()
-            input.openConnection().getInputStream().use { it.copyTo(out) }
-            return newImageCache(out.toByteArray())
+        @Throws(IOException::class)
+        override fun newCache(input: InputStream, formatName: String?): ExternalResource {
+            return input.readBytes().toExternalResource(formatName)
         }
     }
 
@@ -124,87 +73,27 @@ public interface FileCacheStrategy {
          */
         public val directory: File? = null
     ) : FileCacheStrategy {
-        @MiraiExperimentalApi
-        @Throws(java.io.IOException::class)
-        override fun newImageCache(input: Input): ExternalImage {
-            return ExternalImage(File.createTempFile("tmp", null, directory).apply {
-                deleteOnExit()
-                input.withOut(this.outputStream()) { copyTo(it) }
-            }.asReusableInput(true))
+        private fun createTempFile(): File {
+            return File.createTempFile("tmp", null, directory)
         }
 
-        @MiraiExperimentalApi
-        @Throws(java.io.IOException::class)
-        override fun newImageCache(input: InputStream): ExternalImage {
-            return ExternalImage(File.createTempFile("tmp", null, directory).apply {
+        @Throws(IOException::class)
+        override fun newCache(input: InputStream, formatName: String?): ExternalResource {
+            return createTempFile().apply {
                 deleteOnExit()
-                input.withOut(this.outputStream()) { copyTo(it) }
-            }.asReusableInput(true))
+                outputStream().use { out -> input.copyTo(out) }
+            }.toExternalResource(formatName)
         }
-
-        @MiraiExperimentalApi
-        @Throws(java.io.IOException::class)
-        override fun newImageCache(input: ByteArray): ExternalImage {
-            return ExternalImage(input.asReusableInput())
-        }
-
-        @MiraiExperimentalApi
-        override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
-            val file = File.createTempFile("tmp", null, directory).apply { deleteOnExit() }
-
-            val digest = MessageDigest.getInstance("md5")
-            digest.reset()
-
-            file.outputStream().use { out ->
-                ImageIO.write(input, format, object : OutputStream() {
-                    override fun write(b: Int) {
-                        out.write(b)
-                        digest.update(b.toByte())
-                    }
-
-                    override fun write(b: ByteArray) {
-                        out.write(b)
-                        digest.update(b)
-                    }
-
-                    override fun write(b: ByteArray, off: Int, len: Int) {
-                        out.write(b, off, len)
-                        digest.update(b, off, len)
-                    }
-                })
-            }
-
-            @Suppress("DEPRECATION_ERROR")
-            return ExternalImage(file.asReusableInput(true, digest.digest()))
-        }
-
-        @MiraiExperimentalApi
-        override fun newImageCache(input: URL): ExternalImage {
-            return ExternalImage(File.createTempFile("tmp", null, directory).apply {
-                deleteOnExit()
-                input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) }
-            }.asReusableInput(true))
-        }
-    }
-}
-
-
-@Throws(java.io.IOException::class)
-internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
-    var bytesCopied: Long = 0
-    val buffer = ByteArray(bufferSize)
-    var bytes = readAvailable(buffer)
-    while (bytes >= 0) {
-        out.write(buffer, 0, bytes)
-        bytesCopied += bytes
-        bytes = readAvailable(buffer)
     }
-    return bytesCopied
-}
 
-internal inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
-    contract {
-        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    public companion object {
+        /**
+         * 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai 从 [IMirai.FileCacheStrategy] 获取.
+         *
+         * @see IMirai.FileCacheStrategy
+         */
+        @MiraiExperimentalApi
+        @JvmStatic
+        public val PlatformDefault: FileCacheStrategy = TempCache(null)
     }
-    return use { output.use { block(this, output) } }
 }

+ 126 - 0
mirai-core-api/src/commonMain/kotlin/utils/SendImageUtilsJvmKt.kt

@@ -0,0 +1,126 @@
+/*
+ * Copyright 2019-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
+ */
+
+/**
+ * 为 Kotlin 使用者实现的发送图片的一些扩展函数.
+ */
+
+@file:Suppress("unused")
+@file:JvmMultifileClass
+@file:JvmName("SendResourceUtilsJvmKt")
+
+package net.mamoe.mirai.utils
+
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.Image
+import net.mamoe.mirai.message.data.Voice
+import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+import java.io.File
+import java.io.InputStream
+
+// region IMAGE.sendAsImageTo(Contact)
+
+/**
+ * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
+ *
+ * 注意:本函数不会关闭流
+ *
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@JvmSynthetic
+public suspend inline fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> =
+    runBIO {
+        @Suppress("BlockingMethodInNonBlockingContext")
+        toExternalResource("png")
+    }.withUse { sendAsImageTo(contact) }
+
+/**
+ * 将文件作为图片发送到指定联系人
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@JvmSynthetic
+public suspend inline fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
+    require(this.exists() && this.canRead())
+    return toExternalResource("png").withUse { sendAsImageTo(contact) }
+}
+
+// endregion
+
+// region IMAGE.Upload(Contact): Image
+
+/**
+ * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]
+ *
+ * 注意:本函数不会关闭流
+ *
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@JvmSynthetic
+public suspend inline fun InputStream.uploadAsImage(contact: Contact): Image =
+    @Suppress("BlockingMethodInNonBlockingContext")
+    runBIO { toExternalResource("png") }.withUse { uploadAsImage(contact) }
+
+/**
+ * 将文件作为图片上传后构造 [Image]
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@JvmSynthetic
+public suspend inline fun File.uploadAsImage(contact: Contact): Image {
+    require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" }
+    return toExternalResource("png").withUse { uploadAsImage(contact) }
+}
+
+/**
+ * 将文件作为语音上传后构造 [Voice]
+ *
+ * - 请手动关闭输入流
+ * - 请使用 amr 或 silk 格式
+ *
+ * @suppress 注意,这只是个实验性功能且随时可能会删除
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@MiraiExperimentalApi("语音支持处于实验性阶段")
+public suspend inline fun InputStream.uploadAsGroupVoice(group: Group): Voice {
+    return group.uploadVoice(this)
+}
+
+// endregion
+
+// region Contact.uploadImage(IMAGE)
+
+/**
+ * 读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
+ *
+ * 注意:本函数不会关闭流
+ *
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@JvmSynthetic
+public suspend inline fun Contact.uploadImage(imageStream: InputStream): Image =
+    imageStream.uploadAsImage(this@uploadImage)
+
+/**
+ * 将文件作为图片上传, 但不发送
+ * @throws OverFileSizeMaxException
+ */
+@Throws(OverFileSizeMaxException::class)
+@JvmSynthetic
+public suspend inline fun Contact.uploadImage(file: File): Image = file.uploadAsImage(this)
+
+// endregion

+ 0 - 152
mirai-core-api/src/commonMain/kotlin/utils/internal/ChunkedFlowSession.kt

@@ -1,152 +0,0 @@
-/*
- * Copyright 2019-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.internal
-
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.io.ByteReadChannel
-import kotlinx.io.core.ByteReadPacket
-import kotlinx.io.core.Closeable
-import kotlinx.io.core.Input
-import kotlinx.serialization.InternalSerializationApi
-import net.mamoe.mirai.utils.MiraiExperimentalApi
-import java.io.InputStream
-import kotlin.jvm.JvmField
-
-
-@MiraiExperimentalApi
-public interface ChunkedFlowSession<T> : Closeable {
-    public val flow: Flow<T>
-    override fun close()
-}
-
-internal inline fun <T, R> ChunkedFlowSession<T>.map(crossinline mapper: suspend ChunkedFlowSession<T>.(T) -> R): ChunkedFlowSession<R> {
-    return object : ChunkedFlowSession<R> {
-        override val flow: Flow<R> = [email protected] { [email protected](it) }
-        override fun close() = [email protected]()
-    }
-}
-
-
-/**
- * 由 [chunkedFlow] 分割得到的区块
- */
-@MiraiExperimentalApi
-public class ChunkedInput(
-    /**
-     * 区块的数据.
-     * 由 [ByteArrayPool] 缓存并管理, 只可在 [Flow.collect] 中访问.
-     * 它的大小由 [ByteArrayPool.BUFFER_SIZE] 决定, 而有效(有数据)的大小由 [bufferSize] 决定.
-     *
-     * **注意**: 不要将他带出 [Flow.collect] 作用域, 否则将造成内存泄露
-     */
-    @JvmField public val buffer: ByteArray,
-    @JvmField internal var size: Int
-) {
-    /**
-     * [buffer] 的有效大小
-     */
-    public val bufferSize: Int get() = size
-}
-
-/**
- * 创建将 [ByteReadPacket] 以固定大小分割的 [Sequence].
- *
- * 对于一个 1000 长度的 [ByteReadPacket] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence],
- * 其长度分别为: 300, 300, 300, 100.
- *
- * 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence]
- */
-internal fun ByteReadPacket.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
-    ByteArrayPool.checkBufferSize(sizePerPacket)
-    if (this.remaining <= sizePerPacket.toLong()) {
-        return flowOf(
-            ChunkedInput(
-                buffer,
-                this.readAvailable(buffer, 0, sizePerPacket)
-            )
-        )
-    }
-    return flow {
-        val chunkedInput = ChunkedInput(buffer, 0)
-        do {
-            chunkedInput.size = [email protected](buffer, 0, sizePerPacket)
-            emit(chunkedInput)
-        } while ([email protected])
-    }
-}
-
-/**
- * 创建将 [ByteReadChannel] 以固定大小分割的 [Sequence].
- *
- * 对于一个 1000 长度的 [ByteReadChannel] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence],
- * 其长度分别为: 300, 300, 300, 100.
- */
-internal fun ByteReadChannel.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
-    ByteArrayPool.checkBufferSize(sizePerPacket)
-    if (this.isClosedForRead) {
-        return flowOf()
-    }
-    return flow {
-        val chunkedInput = ChunkedInput(buffer, 0)
-        do {
-            chunkedInput.size = [email protected](buffer, 0, sizePerPacket)
-            emit(chunkedInput)
-        } while ([email protected])
-    }
-}
-
-
-/**
- * 创建将 [Input] 以固定大小分割的 [Sequence].
- *
- * 对于一个 1000 长度的 [Input] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence],
- * 其长度分别为: 300, 300, 300, 100.
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-internal fun Input.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
-    ByteArrayPool.checkBufferSize(sizePerPacket)
-
-    if (this.endOfInput) {
-        return flowOf()
-    }
-
-    return flow {
-        val chunkedInput = ChunkedInput(buffer, 0)
-        while ([email protected]) {
-            chunkedInput.size = [email protected](buffer, 0, sizePerPacket)
-            emit(chunkedInput)
-        }
-    }
-}
-
-/**
- * 创建将 [ByteReadPacket] 以固定大小分割的 [Sequence].
- *
- * 对于一个 1000 长度的 [ByteReadPacket] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence],
- * 其长度分别为: 300, 300, 300, 100.
- *
- * 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence]
- */
-@OptIn(ExperimentalCoroutinesApi::class, InternalSerializationApi::class)
-internal fun InputStream.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
-    require(sizePerPacket <= buffer.size) { "sizePerPacket is too large. Maximum buffer size=buffer.size=${buffer.size}" }
-
-    return flow {
-        val chunkedInput = ChunkedInput(buffer, 0)
-        while ([email protected]() != 0) {
-            chunkedInput.size = [email protected](buffer, 0, sizePerPacket)
-            emit(chunkedInput)
-        }
-    }
-}

+ 0 - 65
mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.jvm.kt

@@ -1,65 +0,0 @@
-/*
- * Copyright 2019-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.internal
-
-import io.ktor.utils.io.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.io.core.Input
-import net.mamoe.mirai.utils.FileCacheStrategy
-import java.awt.image.BufferedImage
-import java.io.InputStream
-import java.net.URL
-
-internal actual class DeferredReusableInput actual constructor(
-    val input: Any,
-    val extraArg: Any?
-) : ReusableInput {
-
-
-    actual suspend fun init(strategy: FileCacheStrategy) = withContext(Dispatchers.IO) {
-        if (delegate != null) {
-            return@withContext
-        }
-        delegate = when (input) {
-            is InputStream -> strategy.newImageCache(input)
-            is ByteArray -> strategy.newImageCache(input)
-            is Input -> strategy.newImageCache(input)
-            is URL -> strategy.newImageCache(input)
-            is BufferedImage -> strategy.newImageCache(input, extraArg as String)
-            else -> error("Internal error: unsupported DeferredReusableInput.input: ${input::class.qualifiedName}")
-        }.input
-    }
-
-    private var delegate: ReusableInput? = null
-
-    override val md5: ByteArray
-        get() = delegate?.md5 ?: error("DeferredReusableInput not yet initialized")
-    override val size: Long
-        get() = delegate?.size ?: error("DeferredReusableInput not yet initialized")
-
-    override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
-        return delegate?.chunkedFlow(sizePerPacket) ?: error("DeferredReusableInput not yet initialized")
-    }
-
-    override suspend fun writeTo(out: ByteWriteChannel): Long {
-        return delegate?.writeTo(out) ?: error("DeferredReusableInput not yet initialized")
-    }
-
-    override fun asInput(): Input {
-        return delegate?.asInput() ?: error("DeferredReusableInput not yet initialized")
-    }
-
-    override fun release() {
-        return delegate?.release() ?: error("DeferredReusableInput not yet initialized")
-    }
-
-    actual val initialized: Boolean get() = delegate != null
-}

+ 0 - 28
mirai-core-api/src/commonMain/kotlin/utils/internal/ReusableInput.kt

@@ -1,28 +0,0 @@
-/*
- * Copyright 2019-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.internal
-
-import io.ktor.utils.io.*
-import kotlinx.io.core.Input
-
-internal interface ReusableInput {
-    val md5: ByteArray
-    val size: Long
-
-    fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput>
-    suspend fun writeTo(out: ByteWriteChannel): Long
-
-    /**
-     * Remember to close.
-     */
-    fun asInput(): Input
-
-    fun release()
-}

+ 0 - 145
mirai-core-api/src/commonMain/kotlin/utils/internal/asReusableInput.kt

@@ -1,145 +0,0 @@
-/*
- * Copyright 2019-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.internal
-
-import io.ktor.utils.io.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
-import kotlinx.io.core.Input
-import kotlinx.io.streams.asInput
-import net.mamoe.mirai.message.data.toLongUnsigned
-import java.io.ByteArrayInputStream
-import java.io.File
-import java.io.InputStream
-
-internal fun asReusableInput0(input: ByteArray): ReusableInput = input.asReusableInput()
-
-internal const val DEFAULT_REUSABLE_INPUT_BUFFER_SIZE = 8192
-
-internal fun ByteArray.asReusableInput(): ReusableInput {
-    return object : ReusableInput {
-        override val md5: ByteArray = md5()
-        override val size: Long get() = [email protected]()
-
-        override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
-            return object : ChunkedFlowSession<ChunkedInput> {
-                private val stream = inputStream()
-                override val flow: Flow<ChunkedInput> = stream.chunkedFlow(
-                    sizePerPacket,
-                    ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket))
-                )
-
-                override fun close() {
-                    stream.close()
-                    // nothing to do
-                }
-            }
-        }
-
-        override suspend fun writeTo(out: ByteWriteChannel): Long {
-            out.writeFully(this@asReusableInput, 0, [email protected])
-            out.flush()
-            return [email protected]()
-        }
-
-        override fun asInput(): Input {
-            return ByteArrayInputStream(this@asReusableInput).asInput()
-        }
-
-        override fun release() {
-            // nothing to do
-        }
-    }
-}
-
-internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput {
-    return object : ReusableInput {
-        override val md5: ByteArray = inputStream().use { it.md5() }
-        override val size: Long get() = length()
-
-        override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
-            val stream = inputStream()
-            return object : ChunkedFlowSession<ChunkedInput> {
-                override val flow: Flow<ChunkedInput> = stream.chunkedFlow(
-                    sizePerPacket,
-                    ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket))
-                )
-
-                override fun close() {
-                    stream.close()
-                }
-            }
-        }
-
-        override suspend fun writeTo(out: ByteWriteChannel): Long {
-            return inputStream().use { it.copyTo(out) }
-        }
-
-        override fun asInput(): Input {
-            return inputStream().asInput()
-        }
-
-        override fun release() {
-            if (deleteOnClose) [email protected]()
-        }
-
-    }
-}
-
-internal fun File.asReusableInput(deleteOnClose: Boolean, md5: ByteArray): ReusableInput {
-    return object : ReusableInput {
-        override val md5: ByteArray get() = md5
-        override val size: Long get() = length()
-
-        override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
-            val stream = inputStream()
-            return object : ChunkedFlowSession<ChunkedInput> {
-                override val flow: Flow<ChunkedInput> = stream.chunkedFlow(
-                    sizePerPacket,
-                    ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket))
-                )
-
-                override fun close() {
-                    stream.close()
-                }
-            }
-        }
-
-        override suspend fun writeTo(out: ByteWriteChannel): Long {
-            return inputStream().use { it.copyTo(out) }
-        }
-
-        override fun asInput(): Input {
-            return inputStream().asInput()
-        }
-
-        override fun release() {
-            if (deleteOnClose) [email protected]()
-        }
-    }
-}
-
-private suspend fun InputStream.copyTo(out: ByteWriteChannel): Long = withContext(Dispatchers.IO) {
-    var bytesCopied: Long = 0
-
-    ByteArrayPool.useInstance { buffer ->
-        var bytes = read(buffer)
-        while (bytes >= 0) {
-            out.writeFully(buffer, 0, bytes)
-            bytesCopied += bytes
-            bytes = read(buffer)
-        }
-    }
-
-    out.flush()
-
-    return@withContext bytesCopied
-}

+ 0 - 29
mirai-core-api/src/commonMain/kotlin/utils/internal/md5.jvm.kt

@@ -1,29 +0,0 @@
-/*
- * Copyright 2019-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
- */
-
-@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
-
-package net.mamoe.mirai.utils.internal
-
-import java.io.InputStream
-import java.security.MessageDigest
-
-internal actual fun ByteArray.md5(offset: Int, length: Int): ByteArray {
-    this.checkOffsetAndLength(offset, length)
-    return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
-}
-
-internal actual fun InputStream.md5(): ByteArray {
-    val digest = MessageDigest.getInstance("md5")
-    digest.reset()
-    this.readInSequence { buf, len ->
-        digest.update(buf, 0, len)
-    }
-    return digest.digest()
-}

+ 0 - 156
mirai-core-api/src/commonMain/kotlin/utils/sendTo.kt

@@ -1,156 +0,0 @@
-/*
- * Copyright 2019-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
- */
-
-/**
- * 发送图片的一些扩展函数.
- */
-
-@file:Suppress("unused")
-@file:JvmMultifileClass
-@file:JvmName("SendImageUtilsJvmKt")
-
-package net.mamoe.mirai.utils
-
-import kotlinx.coroutines.Dispatchers
-import net.mamoe.mirai.contact.Contact
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.message.MessageReceipt
-import net.mamoe.mirai.message.data.Image
-import net.mamoe.mirai.message.data.Voice
-import java.awt.image.BufferedImage
-import java.io.File
-import java.io.InputStream
-
-// region IMAGE.sendAsImageTo(Contact)
-
-/**
- * 在 [Dispatchers.IO] 中将图片发送到指定联系人. 不会创建临时文件
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> =
-    toExternalImage().sendTo(contact)
-
-/**
- * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> =
-    toExternalImage().sendTo(contact)
-
-/**
- * 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
-    require(this.exists() && this.canRead())
-    return toExternalImage().sendTo(contact)
-}
-
-// endregion
-
-// region IMAGE.Upload(Contact): Image
-
-/**
- * 在 [Dispatchers.IO] 中将图片上传后构造 [Image]. 不会创建临时文件
- * @throws OverFileSizeMaxException
- */
-@JvmSynthetic
-@Throws(OverFileSizeMaxException::class)
-public suspend fun BufferedImage.upload(contact: Contact): Image =
-    toExternalImage().upload(contact)
-
-/**
- * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend fun InputStream.uploadAsImage(contact: Contact): Image =
-    toExternalImage().upload(contact)
-
-/**
- * 在 [Dispatchers.IO] 中将文件作为图片上传后构造 [Image]
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend fun File.uploadAsImage(contact: Contact): Image {
-    require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" }
-    return toExternalImage().upload(contact)
-}
-
-/**
- * 在 [Dispatchers.IO] 中将文件作为语音上传后构造 [Voice]
- *
- * - 请手动关闭输入流
- * - 请使用 amr 或 silk 格式
- *
- * @suppress 注意,这只是个实验性功能且随时可能会删除
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-@MiraiExperimentalApi("语音支持处于实验性阶段")
-public suspend fun InputStream.uploadAsGroupVoice(group: Group): Voice {
-    return group.uploadVoice(this)
-}
-
-// endregion
-
-// region Contact.sendImage(IMAGE)
-
-/**
- * 在 [Dispatchers.IO] 中将图片发送到指定联系人. 不会保存临时文件
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend inline fun <C : Contact> C.sendImage(bufferedImage: BufferedImage): MessageReceipt<C> =
-    bufferedImage.sendTo(this)
-
-/**
- * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend inline fun <C : Contact> C.sendImage(imageStream: InputStream): MessageReceipt<C> =
-    imageStream.sendAsImageTo(this)
-
-/**
- * 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend inline fun <C : Contact> C.sendImage(file: File): MessageReceipt<C> = file.sendAsImageTo(this)
-
-// endregion
-
-// region Contact.uploadImage(IMAGE)
-
-/**
- * 在 [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend inline fun Contact.uploadImage(bufferedImage: BufferedImage): Image = bufferedImage.upload(this)
-
-/**
- * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend inline fun Contact.uploadImage(imageStream: InputStream): Image = imageStream.uploadAsImage(this)
-
-/**
- * 在 [Dispatchers.IO] 中将文件作为图片上传, 但不发送
- * @throws OverFileSizeMaxException
- */
-@Throws(OverFileSizeMaxException::class)
-public suspend inline fun Contact.uploadImage(file: File): Image = file.uploadAsImage(this)
-
-// endregion

+ 8 - 30
mirai-core-api/src/commonMain/kotlin/utils/internal/md5.common.kt → mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt

@@ -7,60 +7,38 @@
  *  https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
+@file:JvmMultifileClass
+@file:JvmName("MiraiUtils")
 
-package net.mamoe.mirai.utils.internal
+package net.mamoe.mirai.utils
 
 import kotlinx.io.pool.DefaultPool
-import kotlinx.io.pool.ObjectPool
-import java.io.InputStream
-
-internal expect fun InputStream.md5(): ByteArray
-internal expect fun ByteArray.md5(offset: Int = 0, length: Int = this.size - offset): ByteArray
-
-@Suppress("DuplicatedCode") // false positive. `this` is not the same for `List<Byte>` and `ByteArray`
-internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) {
-    require(offset >= 0) { "offset shouldn't be negative: $offset" }
-    require(length >= 0) { "length shouldn't be negative: $length" }
-    require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" }
-}
-
-
-internal inline fun InputStream.readInSequence(block: (ByteArray, len: Int) -> Unit) {
-    var read: Int
-    ByteArrayPool.useInstance { buf ->
-        while (this.read(buf).also { read = it } != -1) {
-            block(buf, read)
-        }
-    }
-}
-
 
 /**
  * 缓存 [ByteArray] 实例的 [ObjectPool]
  */
-internal object ByteArrayPool : DefaultPool<ByteArray>(256) {
+public object ByteArrayPool : DefaultPool<ByteArray>(256) {
     /**
      * 每一个 [ByteArray] 的大小
      */
-    const val BUFFER_SIZE: Int = 8192 * 8
+    public const val BUFFER_SIZE: Int = 8192 * 8
 
     override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE)
 
     override fun clearInstance(instance: ByteArray): ByteArray = instance
 
-    fun checkBufferSize(size: Int) {
+    public fun checkBufferSize(size: Int) {
         require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" }
     }
 
-    fun checkBufferSize(size: Long) {
+    public fun checkBufferSize(size: Long) {
         require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" }
     }
 
     /**
      * 请求一个大小至少为 [requestedSize] 的 [ByteArray] 实例.
      */ // 不要写为扩展函数. 它需要优先于 kotlinx.io 的扩展函数 resolve
-    inline fun <R> useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R {
+    public inline fun <R> useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R {
         if (requestedSize > BUFFER_SIZE) {
             return ByteArray(requestedSize).run(block)
         }

+ 68 - 0
mirai-core-utils/src/commonMain/kotlin/Bytes.kt

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019-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
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("MiraiUtils")
+
+package net.mamoe.mirai.utils
+
+
+@JvmOverloads
+public fun generateImageId(md5: ByteArray, format: String = "mirai"): String {
+    return """{${generateUUID(md5)}}.$format"""
+}
+
+public fun generateUUID(md5: ByteArray): String {
+    return "${md5[0, 3]}-${md5[4, 5]}-${md5[6, 7]}-${md5[8, 9]}-${md5[10, 15]}"
+}
+
+@JvmSynthetic
+internal operator fun ByteArray.get(rangeStart: Int, rangeEnd: Int): String = buildString {
+    for (it in rangeStart..rangeEnd) {
+        append(this@get[it].fixToString())
+    }
+}
+
+private fun Byte.fixToString(): String {
+    return when (val b = this.toInt() and 0xff) {
+        in 0..15 -> "0${this.toString(16).toUpperCase()}"
+        else -> b.toString(16).toUpperCase()
+    }
+}
+
+@OptIn(ExperimentalUnsignedTypes::class)
+@JvmOverloads
+@Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray
+public fun ByteArray.toUHexString(
+    separator: String = " ",
+    offset: Int = 0,
+    length: Int = this.size - offset
+): String {
+    this.checkOffsetAndLength(offset, length)
+    if (length == 0) {
+        return ""
+    }
+    val lastIndex = offset + length
+    return buildString(length * 2) {
+        [email protected] { index, it ->
+            if (index in offset until lastIndex) {
+                var ret = it.toUByte().toString(16).toUpperCase()
+                if (ret.length == 1) ret = "0$ret"
+                append(ret)
+                if (index < lastIndex - 1) append(separator)
+            }
+        }
+    }
+}
+
+public fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) {
+    require(offset >= 0) { "offset shouldn't be negative: $offset" }
+    require(length >= 0) { "length shouldn't be negative: $length" }
+    require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" }
+}

+ 9 - 8
mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.common.kt → mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt

@@ -7,14 +7,15 @@
  *  https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.utils.internal
+@file:JvmMultifileClass
+@file:JvmName("MiraiUtils")
 
-import net.mamoe.mirai.utils.FileCacheStrategy
+package net.mamoe.mirai.utils
 
-internal expect class DeferredReusableInput(input: Any, extraArg: Any?) : ReusableInput {
-    val initialized: Boolean
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 
-
-    suspend fun init(strategy: FileCacheStrategy)
-
-}
+public suspend inline fun <R> runBIO(
+    noinline block: suspend CoroutineScope.() -> R
+): R = withContext(Dispatchers.IO, block)

+ 46 - 0
mirai-core-utils/src/commonMain/kotlin/Files.kt

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019-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
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("MiraiUtils")
+
+package net.mamoe.mirai.utils
+
+/**
+ * 文件头和文件类型列表
+ */
+public val FILE_TYPES: MutableMap<String, String> = mutableMapOf(
+    "FFD8FF" to "jpg",
+    "89504E47" to "png",
+    "47494638" to "gif",
+    "49492A00" to "tif",
+    "424D" to "bmp",
+    "57415645" to "wav",
+)
+
+/*
+
+        startsWith("FFD8") -> "jpg"
+        startsWith("89504E47") -> "png"
+        startsWith("47494638") -> "gif"
+        startsWith("424D") -> "bmp"
+ */
+
+/**
+ * 根据文件头获取文件类型
+ */
+public fun getFileType(fileHeader: ByteArray): String? {
+    val hex = fileHeader.toUHexString("")
+    FILE_TYPES.forEach { (k, v) ->
+        if (hex.startsWith(k)) {
+            return v
+        }
+    }
+    return null
+}

+ 146 - 0
mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt

@@ -0,0 +1,146 @@
+/*
+ * Copyright 2019-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
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("MiraiUtils")
+
+package net.mamoe.mirai.utils
+
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import kotlinx.io.core.Input
+import kotlinx.io.core.readAvailable
+import java.io.*
+import java.net.Inet4Address
+import java.security.MessageDigest
+import java.util.zip.Deflater
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
+import java.util.zip.Inflater
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+public object MiraiPlatformUtils {
+    /**
+     * Ktor HttpClient. 不同平台使用不同引擎.
+     */
+    public val Http: HttpClient = HttpClient(CIO)
+}
+
+@JvmOverloads
+public fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteArray {
+    checkOffsetAndLength(offset, length)
+    if (length == 0) return ByteArray(0)
+
+    val inflater = Inflater()
+    inflater.reset()
+    ByteArrayOutputStream().use { output ->
+        inflater.setInput(this, offset, length)
+        ByteArray(DEFAULT_BUFFER_SIZE).let {
+            while (!inflater.finished()) {
+                output.write(it, 0, inflater.inflate(it))
+            }
+        }
+
+        inflater.end()
+        return output.toByteArray()
+    }
+}
+
+public fun InputStream.md5(): ByteArray {
+    val digest = MessageDigest.getInstance("md5")
+    digest.reset()
+    use { input ->
+        object : OutputStream() {
+            override fun write(b: Int) {
+                digest.update(b.toByte())
+            }
+
+            override fun write(b: ByteArray, off: Int, len: Int) {
+                digest.update(b, off, len)
+            }
+        }.use { output ->
+            input.copyTo(output)
+        }
+    }
+    return digest.digest()
+}
+
+/**
+ * Localhost 解析
+ */
+public fun localIpAddress(): String = runCatching {
+    Inet4Address.getLocalHost().hostAddress
+}.getOrElse { "192.168.1.123" }
+
+public fun String.md5(): ByteArray = toByteArray().md5()
+
+@JvmOverloads
+public fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArray {
+    checkOffsetAndLength(offset, length)
+    return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
+}
+
+@JvmOverloads
+public fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray {
+    return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() }
+}
+
+@JvmOverloads
+public fun ByteArray.gzip(offset: Int = 0, length: Int = size - offset): ByteArray {
+    ByteArrayOutputStream().use { buf ->
+        GZIPOutputStream(buf).use { gzip ->
+            inputStream(offset, length).use { t -> t.copyTo(gzip) }
+        }
+        buf.flush()
+        return buf.toByteArray()
+    }
+}
+
+@JvmOverloads
+public fun ByteArray.zip(offset: Int = 0, length: Int = size - offset): ByteArray {
+    checkOffsetAndLength(offset, length)
+    if (length == 0) return ByteArray(0)
+
+    val deflater = Deflater()
+    deflater.setInput(this, offset, length)
+    deflater.finish()
+
+    ByteArray(DEFAULT_BUFFER_SIZE).let {
+        return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
+    }
+}
+
+public inline fun <C : Closeable, R> C.withUse(block: C.() -> R): R {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    return use(block)
+}
+
+@Throws(IOException::class)
+@JvmOverloads
+public fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
+    var bytesCopied: Long = 0
+    val buffer = ByteArray(bufferSize)
+    var bytes = readAvailable(buffer)
+    while (bytes >= 0) {
+        out.write(buffer, 0, bytes)
+        bytesCopied += bytes
+        bytes = readAvailable(buffer)
+    }
+    return bytesCopied
+}
+
+public inline fun <I : AutoCloseable, O : AutoCloseable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    return use { output.use { block(this, output) } }
+}

+ 1 - 0
mirai-core-api/src/commonTest/kotlin/utils/ExternalImageTest.kt → mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ExternalImageTest.kt

@@ -6,6 +6,7 @@
  *
  *  https://github.com/mamoe/mirai/blob/master/LICENSE
  */
+
 package net.mamoe.mirai.utils
 
 import kotlin.test.Test

+ 2 - 4
mirai-core/src/commonMain/kotlin/BotAccount.kt

@@ -10,10 +10,8 @@
 
 package net.mamoe.mirai.internal
 
-import kotlinx.io.core.toByteArray
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
 import net.mamoe.mirai.utils.MiraiExperimentalApi
-import kotlin.jvm.JvmSynthetic
+import net.mamoe.mirai.utils.md5
 
 internal data class BotAccount(
     @JvmSynthetic
@@ -22,7 +20,7 @@ internal data class BotAccount(
     @MiraiExperimentalApi
     val passwordMd5: ByteArray // md5
 ) {
-    constructor(id: Long, passwordPlainText: String) : this(id, MiraiPlatformUtils.md5(passwordPlainText.toByteArray()))
+    constructor(id: Long, passwordPlainText: String) : this(id, passwordPlainText.md5())
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other == null || this::class != other::class) return false

+ 6 - 8
mirai-core/src/commonMain/kotlin/MiraiImpl.kt

@@ -30,7 +30,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.*
 import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
 import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
 import net.mamoe.mirai.internal.utils.encodeToString
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.message.MessageReceipt
@@ -39,10 +38,8 @@ import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1
 import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2
 import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX
-import net.mamoe.mirai.utils.BotConfiguration
-import net.mamoe.mirai.utils.MiraiExperimentalApi
-import net.mamoe.mirai.utils.cast
-import net.mamoe.mirai.utils.currentTimeSeconds
+import net.mamoe.mirai.utils.*
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
 import java.util.concurrent.atomic.AtomicBoolean
 import kotlin.math.absoluteValue
 import kotlin.random.Random
@@ -62,6 +59,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
     override val BotFactory: BotFactory
         get() = BotFactoryImpl
 
+    override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault
+
     @OptIn(LowLevelApi::class)
     override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
         @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
@@ -609,8 +608,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
                         bot,
                         response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
                         response.proto.msgSig,
-                        MiraiPlatformUtils.md5(body),
-                        net.mamoe.mirai.utils.internal.asReusableInput0(body), // don't use toLongUnsigned: Overload resolution ambiguity
+                        body.md5(),
+                        body.toExternalResource(null), // don't use toLongUnsigned: Overload resolution ambiguity
                         "group long message",
                         27
                     )
@@ -801,7 +800,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
         else -> error("Internal error: unsupported image class: ${image::class.simpleName}")
     }
 
-    @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER")
     override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean {
         if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
             throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE")

+ 17 - 21
mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt

@@ -15,14 +15,15 @@ import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.BeforeImageUploadEvent
 import net.mamoe.mirai.event.events.EventCancelledException
 import net.mamoe.mirai.event.events.ImageUploadEvent
+import net.mamoe.mirai.internal.message.OfflineFriendImage
 import net.mamoe.mirai.internal.network.highway.postImage
 import net.mamoe.mirai.internal.network.highway.sizeToString
 import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352
 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
 import net.mamoe.mirai.internal.utils.toUHexString
 import net.mamoe.mirai.message.data.Image
-import net.mamoe.mirai.utils.ExternalImage
+import net.mamoe.mirai.utils.ExternalResource
+import net.mamoe.mirai.utils.MiraiPlatformUtils
 import net.mamoe.mirai.utils.verbose
 import kotlin.coroutines.CoroutineContext
 import kotlin.math.roundToInt
@@ -38,11 +39,8 @@ internal abstract class AbstractUser(
     final override val remark: String = friendInfo.remark
 
     @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-    override suspend fun uploadImage(image: ExternalImage): Image = try {
-        if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) {
-            image.input.init(bot.configuration.fileCacheStrategy)
-        }
-        if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
+    override suspend fun uploadImage(resource: ExternalResource): Image {
+        if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
             throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
         }
         val response = bot.network.run {
@@ -51,22 +49,22 @@ internal abstract class AbstractUser(
                     srcUin = bot.id.toInt(),
                     dstUin = id.toInt(),
                     fileId = 0,
-                    fileMd5 = image.md5,
-                    fileSize = image.input.size.toInt(),
-                    fileName = image.md5.toUHexString("") + "." + image.formatName,
+                    fileMd5 = resource.md5,
+                    fileSize = resource.size.toInt(),
+                    fileName = resource.md5.toUHexString("") + "." + resource.formatName,
                     imgOriginal = 1
                 )
             ).sendAndExpect<LongConn.OffPicUp.Response>()
         }
 
-        when (response) {
-            is LongConn.OffPicUp.Response.FileExists -> net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId)
+        return when (response) {
+            is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(response.resourceId)
                 .also {
-                    ImageUploadEvent.Succeed(this, image, it).broadcast()
+                    ImageUploadEvent.Succeed(this, resource, it).broadcast()
                 }
             is LongConn.OffPicUp.Response.RequireUpload -> {
                 bot.network.logger.verbose {
-                    "[Http] Uploading friend image, size=${image.input.size.sizeToString()}"
+                    "[Http] Uploading friend image, size=${resource.size.sizeToString()}"
                 }
 
                 val time = measureTime {
@@ -74,13 +72,13 @@ internal abstract class AbstractUser(
                         "0x6ff0070",
                         bot.id,
                         null,
-                        imageInput = image.input,
+                        imageInput = resource,
                         uKeyHex = response.uKey.toUHexString("")
                     )
                 }
 
                 bot.network.logger.verbose {
-                    "[Http] Uploading friend image: succeed at ${(image.input.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
+                    "[Http] Uploading friend image: succeed at ${(resource.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
                 }
 
                 /*
@@ -94,16 +92,14 @@ internal abstract class AbstractUser(
                 )*/
                 // 为什么不能 ??
 
-                net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId).also {
-                    ImageUploadEvent.Succeed(this, image, it).broadcast()
+                OfflineFriendImage(response.resourceId).also {
+                    ImageUploadEvent.Succeed(this, resource, it).broadcast()
                 }
             }
             is LongConn.OffPicUp.Response.Failed -> {
-                ImageUploadEvent.Failed(this, image, -1, response.message).broadcast()
+                ImageUploadEvent.Failed(this, resource, -1, response.message).broadcast()
                 error(response.message)
             }
         }
-    } finally {
-        image.input.release()
     }
 }

+ 2 - 2
mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt

@@ -18,7 +18,7 @@ import net.mamoe.mirai.internal.MiraiImpl
 import net.mamoe.mirai.message.action.MemberNudge
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.Message
-import net.mamoe.mirai.utils.ExternalImage
+import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.MemberDeprecatedApi
 import kotlin.coroutines.CoroutineContext
 
@@ -39,7 +39,7 @@ internal class AnonymousMemberImpl(
     override val remark: String get() = memberInfo.remark
 
     override fun nudge(): MemberNudge = notSupported("Nudge")
-    override suspend fun uploadImage(image: ExternalImage): Image = notSupported("Upload image to")
+    override suspend fun uploadImage(resource: ExternalResource): Image = notSupported("Upload image to")
     override suspend fun unmute() {
     }
 

+ 13 - 20
mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt

@@ -24,6 +24,7 @@ import net.mamoe.mirai.internal.message.MessageSourceToGroupImpl
 import net.mamoe.mirai.internal.message.OfflineGroupImage
 import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable
 import net.mamoe.mirai.internal.message.firstIsInstanceOrNull
+import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
 import net.mamoe.mirai.internal.network.highway.HighwayHelper
 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
 import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
@@ -31,9 +32,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGro
 import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
 import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
 import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
 import net.mamoe.mirai.internal.utils.estimateLength
-import net.mamoe.mirai.internal.utils.toUHexString
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.*
@@ -228,53 +227,47 @@ internal class GroupImpl(
         return result.getOrThrow()
     }
 
-    @Suppress("DEPRECATION", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
     @OptIn(ExperimentalTime::class)
-    override suspend fun uploadImage(image: ExternalImage): Image = try {
-        if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) {
-            image.input.init(bot.configuration.fileCacheStrategy)
-        }
-        if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
+    override suspend fun uploadImage(resource: ExternalResource): Image {
+        if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
             throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
         }
-        bot.network.run {
+        bot.network.run<QQAndroidBotNetworkHandler, Image> {
             val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
                 bot.client,
                 uin = bot.id,
                 groupCode = id,
-                md5 = image.md5,
-                size = image.input.size.toInt()
+                md5 = resource.md5,
+                size = resource.size.toInt()
             ).sendAndExpect()
 
             @Suppress("UNCHECKED_CAST") // bug
             when (response) {
                 is ImgStore.GroupPicUp.Response.Failed -> {
-                    ImageUploadEvent.Failed(this@GroupImpl, image, response.resultCode, response.message).broadcast()
+                    ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast()
                     if (response.message == "over file size max") throw OverFileSizeMaxException()
                     error("upload group image failed with reason ${response.message}")
                 }
                 is ImgStore.GroupPicUp.Response.FileExists -> {
-                    val resourceId = image.calculateImageResourceId()
+                    val resourceId = resource.calculateResourceId()
                     return OfflineGroupImage(imageId = resourceId)
-                        .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
+                        .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
                 }
                 is ImgStore.GroupPicUp.Response.RequireUpload -> {
                     HighwayHelper.uploadImageToServers(
                         bot,
                         response.uploadIpList.zip(response.uploadPortList),
                         response.uKey,
-                        image.input,
+                        resource,
                         kind = "group image",
                         commandId = 2
                     )
-                    val resourceId = image.calculateImageResourceId()
+                    val resourceId = resource.calculateResourceId()
                     return OfflineGroupImage(imageId = resourceId)
-                        .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
+                        .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
                 }
             }
         }
-    } finally {
-        image.input.release()
     }
 
     /**
@@ -290,7 +283,7 @@ internal class GroupImpl(
         if (content.size > 1048576) {
             throw  OverFileSizeMaxException()
         }
-        val md5 = MiraiPlatformUtils.md5(content)
+        val md5 = content.md5()
         val codec = with(content.copyOfRange(0, 10).toUHexString("")) {
             when {
                 startsWith("2321414D52") -> 0             // amr

+ 2 - 2
mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt

@@ -15,7 +15,7 @@ import net.mamoe.mirai.contact.OtherClientInfo
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.Message
-import net.mamoe.mirai.utils.ExternalImage
+import net.mamoe.mirai.utils.ExternalResource
 import kotlin.coroutines.CoroutineContext
 
 internal class OtherClientImpl(
@@ -27,7 +27,7 @@ internal class OtherClientImpl(
         throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.")
     }
 
-    override suspend fun uploadImage(image: ExternalImage): Image {
+    override suspend fun uploadImage(resource: ExternalResource): Image {
         throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.")
     }
 

+ 5 - 3
mirai-core/src/commonMain/kotlin/message/conversions.kt

@@ -28,6 +28,8 @@ import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.safeCast
+import net.mamoe.mirai.utils.unzip
+import net.mamoe.mirai.utils.zip
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
@@ -58,7 +60,7 @@ internal fun MessageChain.toRichTextElems(
 
     fun transformOneMessage(it: Message) {
         if (it is RichMessage) {
-            val content = MiraiPlatformUtils.zip(it.content.toByteArray())
+            val content = it.content.toByteArray().zip()
             when (it) {
                 is ForwardMessageInternal -> {
                     elements.add(
@@ -422,7 +424,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, botId:
                     { "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) {
                     when (element.lightApp.data[0].toInt()) {
                         0 -> element.lightApp.data.encodeToString(offset = 1)
-                        1 -> MiraiPlatformUtils.unzip(element.lightApp.data, 1).encodeToString()
+                        1 -> element.lightApp.data.unzip(1).encodeToString()
                         else -> error("unknown compression flag=${element.lightApp.data[0]}")
                     }
                 }
@@ -432,7 +434,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, botId:
                 val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) {
                     when (element.richMsg.template1[0].toInt()) {
                         0 -> element.richMsg.template1.encodeToString(offset = 1)
-                        1 -> MiraiPlatformUtils.unzip(element.richMsg.template1, 1).encodeToString()
+                        1 -> element.richMsg.template1.unzip(1).encodeToString()
                         else -> error("unknown compression flag=${element.richMsg.template1[0]}")
                     }
                 }

+ 17 - 7
mirai-core/src/commonMain/kotlin/message/imagesImpl.kt

@@ -25,19 +25,31 @@ import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1
 import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2
 import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX
 import net.mamoe.mirai.message.data.md5
-import net.mamoe.mirai.utils.ExternalImage
+import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.MiraiExperimentalApi
+import net.mamoe.mirai.utils.generateImageId
+
+/*
+ * ImgType:
+ *  JPG:    1000
+ *  PNG:    1001
+ *  WEBP:   1002
+ *  BMP:    1005
+ *  GIG:    2000 // gig? gif?
+ *  APNG:   2001
+ *  SHARPP: 1004
+ */
 
 internal class OnlineGroupImageImpl(
     internal val delegate: ImMsgBody.CustomFace
 ) : @Suppress("DEPRECATION")
 OnlineGroupImage() {
-    override val imageId: String = ExternalImage.generateImageId(
+    override val imageId: String = generateImageId(
         delegate.md5,
         delegate.filePath.substringAfterLast('.')
     ).takeIf {
         GROUP_IMAGE_ID_REGEX.matches(it)
-    } ?: ExternalImage.generateImageId(delegate.md5)
+    } ?: generateImageId(delegate.md5)
 
     override val originUrl: String
         get() = if (delegate.origUrl.isBlank()) {
@@ -145,14 +157,12 @@ internal interface SuspendDeferredOriginUrlAware : Image {
 @MiraiExperimentalApi("Will be renamed to OfflineImage on 1.2.0")
 @Suppress("DEPRECATION_ERROR")
 internal class ExperimentalDeferredImage internal constructor(
-    @Suppress("CanBeParameter") private val externalImage: ExternalImage // for future use
+    @Suppress("CanBeParameter") private val externalImage: ExternalResource // for future use
 ) : AbstractImage(), SuspendDeferredOriginUrlAware {
     override suspend fun getUrl(bot: Bot): String {
         TODO()
     }
-
-    @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-    override val imageId: String = externalImage.calculateImageResourceId()
+    override val imageId: String = externalImage.calculateResourceId()
 }
 
 @Suppress("EXPOSED_SUPER_INTERFACE")

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt

@@ -38,7 +38,7 @@ internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddre
  */
 @Suppress("RemoveRedundantQualifierName") // bug
 private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
-    net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(androidId + macAddress)
+    (androidId + macAddress).md5()
 
 /**
  * 生成长度为 [length], 元素为随机 `0..255` 的 [ByteArray]
@@ -317,7 +317,7 @@ internal open class QQAndroidClient(
 
 @Suppress("RemoveRedundantQualifierName") // bug
 internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
-    net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(getRandomByteArray(16) + guid)
+    (getRandomByteArray(16) + guid).md5()
 
 
 internal class ReserveUinInfo(

+ 109 - 29
mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt

@@ -7,8 +7,6 @@
  *  https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-
 package net.mamoe.mirai.internal.network.highway
 
 import io.ktor.client.*
@@ -16,21 +14,25 @@ import io.ktor.client.request.*
 import io.ktor.http.*
 import io.ktor.http.content.*
 import io.ktor.utils.io.*
+import io.ktor.utils.io.jvm.javaio.*
 import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.io.core.discardExact
-import kotlinx.io.core.use
+import kotlinx.io.core.*
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.network.QQAndroidClient
 import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead
-import net.mamoe.mirai.internal.utils.*
+import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
+import net.mamoe.mirai.internal.utils.PlatformSocket
+import net.mamoe.mirai.internal.utils.SocketException
+import net.mamoe.mirai.internal.utils.addSuppressedMirai
 import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
+import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.internal.utils.io.withUse
-import net.mamoe.mirai.utils.internal.ReusableInput
-import net.mamoe.mirai.utils.verbose
+import net.mamoe.mirai.internal.utils.toIpV4AddressString
+import net.mamoe.mirai.utils.*
+import java.io.InputStream
 import kotlin.math.roundToInt
 import kotlin.time.ExperimentalTime
 import kotlin.time.measureTime
@@ -41,7 +43,7 @@ internal suspend fun HttpClient.postImage(
     htcmd: String,
     uin: Long,
     groupcode: Long?,
-    imageInput: ReusableInput,
+    imageInput: ExternalResource,
     uKeyHex: String
 ): Boolean = post<HttpStatusCode> {
     url {
@@ -65,12 +67,10 @@ internal suspend fun HttpClient.postImage(
 
     body = object : OutgoingContent.WriteChannelContent() {
         override val contentType: ContentType = ContentType.Image.Any
-        override val contentLength: Long = imageInput.size
-
+        override val contentLength: Long = imageInput.size.toLong()
 
         override suspend fun writeTo(channel: ByteWriteChannel) {
-            imageInput.writeTo(channel)
-
+            imageInput.inputStream().withUse { copyTo(channel) }
         }
     }
 } == HttpStatusCode.OK
@@ -82,7 +82,7 @@ internal object HighwayHelper {
         bot: QQAndroidBot,
         servers: List<Pair<Int, Int>>,
         uKey: ByteArray,
-        image: ReusableInput,
+        image: ExternalResource,
         kind: String,
         commandId: Int
     ) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId)
@@ -94,11 +94,11 @@ internal object HighwayHelper {
         servers: List<Pair<Int, Int>>,
         uKey: ByteArray,
         md5: ByteArray,
-        input: ReusableInput,
+        input: ExternalResource,
         kind: String,
         commandId: Int
     ) = servers.retryWithServers(
-        (input.size * 1000 / 1024 / 10).coerceAtLeast(5000),
+        (input.size * 1000 / 1024 / 10).coerceAtLeast(5000).toLong(),
         onFail = {
             throw IllegalStateException("cannot upload $kind, failed on all servers.", it)
         }
@@ -131,7 +131,7 @@ internal object HighwayHelper {
         serverIp: String,
         serverPort: Int,
         ticket: ByteArray,
-        imageInput: ReusableInput,
+        imageInput: ExternalResource,
         fileMd5: ByteArray,
         commandId: Int  // group=2, friend=1
     ) {
@@ -157,18 +157,16 @@ internal object HighwayHelper {
                 ticket = ticket,
                 data = imageInput,
                 fileMd5 = fileMd5
-            ).withUse {
-                flow.collect {
-                    socket.send(it)
-                    //0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
-
-                    socket.read().withUse {
-                        discardExact(1)
-                        val headLength = readInt()
-                        discardExact(4)
-                        val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
-                        check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" }
-                    }
+            ).useAll {
+                socket.send(it)
+                //0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
+
+                socket.read().withUse {
+                    discardExact(1)
+                    val headLength = readInt()
+                    discardExact(4)
+                    val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
+                    check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" }
                 }
             }
         }
@@ -223,6 +221,87 @@ internal object HighwayHelper {
     }
 }
 
+internal class ChunkedFlowSession<T>(
+    private val input: InputStream,
+    private val buffer: ByteArray,
+    private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T
+) : Closeable {
+    override fun close() {
+        input.close()
+    }
+
+    private var offset = 0L
+
+    @Suppress("BlockingMethodInNonBlockingContext")
+    internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
+        runBIO {
+            val size = input.read(buffer)
+            block(mapper(buffer, size, offset))
+            offset += size
+        }
+    }
+}
+
+
+internal fun createImageDataPacketSequence(
+    // RequestDataTrans
+    client: QQAndroidClient,
+    command: String,
+    appId: Int,
+    dataFlag: Int = 4096,
+    commandId: Int,
+    localId: Int = 2052,
+    ticket: ByteArray,
+    data: ExternalResource,
+    fileMd5: ByteArray,
+    sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE
+): ChunkedFlowSession<ByteReadPacket> {
+    ByteArrayPool.checkBufferSize(sizePerPacket)
+    //   require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
+
+    return ChunkedFlowSession(data.inputStream(), ByteArray(sizePerPacket)) { buffer, size, offset ->
+        val head = CSDataHighwayHead.ReqDataHighwayHead(
+            msgBasehead = CSDataHighwayHead.DataHighwayHead(
+                version = 1,
+                uin = client.uin.toString(),
+                command = command,
+                seq = when (commandId) {
+                    2 -> client.nextHighwayDataTransSequenceIdForGroup()
+                    1 -> client.nextHighwayDataTransSequenceIdForFriend()
+                    27 -> client.nextHighwayDataTransSequenceIdForApplyUp()
+                    else -> error("illegal commandId: $commandId")
+                },
+                retryTimes = 0,
+                appid = appId,
+                dataflag = dataFlag,
+                commandId = commandId,
+                localeId = localId
+            ),
+            msgSeghead = CSDataHighwayHead.SegHead(
+                //   cacheAddr = 812157193,
+                datalength = size,
+                dataoffset = offset,
+                filesize = data.size,
+                serviceticket = ticket,
+                md5 = buffer.md5(0, size),
+                fileMd5 = fileMd5,
+                flag = 0,
+                rtcode = 0
+            ),
+            reqExtendinfo = EMPTY_BYTE_ARRAY,
+            msgLoginSigHead = null
+        ).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer())
+
+        buildPacket {
+            writeByte(40)
+            writeInt(head.size)
+            writeInt(size)
+            writeFully(head)
+            writeFully(buffer, 0, size)
+            writeByte(41)
+        }
+    }
+}
 
 internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
     timeoutMillis: Long,
@@ -249,6 +328,7 @@ internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
     onFail(exception)
 }
 
+internal fun Int.sizeToString() = this.toLong().sizeToString()
 internal fun Long.sizeToString(): String {
     return if (this < 1024) {
         "$this B"

+ 0 - 92
mirai-core/src/commonMain/kotlin/network/highway/highway.kt

@@ -1,92 +0,0 @@
-/*
- * Copyright 2019-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
- */
-
-@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-
-package net.mamoe.mirai.internal.network.highway
-
-import kotlinx.io.core.ByteReadPacket
-import kotlinx.io.core.buildPacket
-import kotlinx.io.core.writeFully
-import net.mamoe.mirai.internal.network.QQAndroidClient
-import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead
-import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
-import net.mamoe.mirai.internal.utils.ByteArrayPool
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
-import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
-import net.mamoe.mirai.utils.internal.ChunkedFlowSession
-import net.mamoe.mirai.utils.internal.ChunkedInput
-import net.mamoe.mirai.utils.internal.ReusableInput
-import net.mamoe.mirai.utils.internal.map
-
-
-internal fun createImageDataPacketSequence(
-    // RequestDataTrans
-    client: QQAndroidClient,
-    command: String,
-    appId: Int,
-    dataFlag: Int = 4096,
-    commandId: Int,
-    localId: Int = 2052,
-    ticket: ByteArray,
-    data: ReusableInput,
-    fileMd5: ByteArray,
-    sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE
-): ChunkedFlowSession<ByteReadPacket> {
-    ByteArrayPool.checkBufferSize(sizePerPacket)
-    //   require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
-
-    val session: ChunkedFlowSession<ChunkedInput> = data.chunkedFlow(sizePerPacket)
-
-    var offset = 0L
-    return session.map { chunkedInput ->
-        buildPacket {
-            val head = CSDataHighwayHead.ReqDataHighwayHead(
-                msgBasehead = CSDataHighwayHead.DataHighwayHead(
-                    version = 1,
-                    uin = client.uin.toString(),
-                    command = command,
-                    seq = when (commandId) {
-                        2 -> client.nextHighwayDataTransSequenceIdForGroup()
-                        1 -> client.nextHighwayDataTransSequenceIdForFriend()
-                        27 -> client.nextHighwayDataTransSequenceIdForApplyUp()
-                        else -> error("illegal commandId: $commandId")
-                    },
-                    retryTimes = 0,
-                    appid = appId,
-                    dataflag = dataFlag,
-                    commandId = commandId,
-                    localeId = localId
-                ),
-                msgSeghead = CSDataHighwayHead.SegHead(
-                    //   cacheAddr = 812157193,
-                    datalength = chunkedInput.bufferSize,
-                    dataoffset = offset,
-                    filesize = data.size,
-                    serviceticket = ticket,
-                    md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize),
-                    fileMd5 = fileMd5,
-                    flag = 0,
-                    rtcode = 0
-                ),
-                reqExtendinfo = EMPTY_BYTE_ARRAY,
-                msgLoginSigHead = null
-            ).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer())
-
-            offset += chunkedInput.bufferSize
-
-            writeByte(40)
-            writeInt(head.size)
-            writeInt(chunkedInput.bufferSize)
-            writeFully(head)
-            writeFully(chunkedInput.buffer, 0, chunkedInput.bufferSize)
-            writeByte(41)
-        }
-    }
-}

+ 4 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt

@@ -25,13 +25,15 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.Heartbeat
 import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.internal.network.readUShortLVByteArray
-import net.mamoe.mirai.internal.utils.*
 import net.mamoe.mirai.internal.utils.crypto.TEA
 import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
 import net.mamoe.mirai.internal.utils.io.readPacketExact
 import net.mamoe.mirai.internal.utils.io.readString
 import net.mamoe.mirai.internal.utils.io.useBytes
 import net.mamoe.mirai.internal.utils.io.withUse
+import net.mamoe.mirai.internal.utils.toInt
+import net.mamoe.mirai.internal.utils.toReadPacket
+import net.mamoe.mirai.internal.utils.toUHexString
 import net.mamoe.mirai.utils.*
 
 internal sealed class PacketFactory<TPacket : Packet?> {
@@ -348,7 +350,7 @@ internal object KnownPacketFactories {
             1 -> {
                 input.discardExact(4)
                 input.useBytes { data, length ->
-                    MiraiPlatformUtils.unzip(data, 0, length).let {
+                    data.unzip(0, length).let {
                         val size = it.toInt()
                         if (size == it.size || size == it.size + 4) {
                             it.toReadPacket(offset = 4)

+ 8 - 6
mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt

@@ -16,11 +16,11 @@ import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.toByteArray
 import kotlinx.io.core.writeFully
 import net.mamoe.mirai.internal.network.protocol.LoginType
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
 import net.mamoe.mirai.internal.utils.NetworkType
 import net.mamoe.mirai.internal.utils.io.*
 import net.mamoe.mirai.internal.utils.toByteArray
 import net.mamoe.mirai.utils.currentTimeMillis
+import net.mamoe.mirai.utils.md5
 import kotlin.random.Random
 
 /**
@@ -101,8 +101,10 @@ internal fun BytePacketBuilder.t106(
     guid?.requireSize(16)
 
     writeShortLVPacket {
-        encryptAndWrite(MiraiPlatformUtils.md5(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt()
-            .toByteArray())) {
+        encryptAndWrite(
+            (passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt()
+                .toByteArray()).md5()
+        ) {
             writeShort(4)//TGTGTVer
             writeInt(Random.nextInt())
             writeInt(ssoVersion)//ssoVer
@@ -335,7 +337,7 @@ internal fun BytePacketBuilder.t109(
 ) {
     writeShort(0x109)
     writeShortLVPacket {
-        writeFully(MiraiPlatformUtils.md5(androidId))
+        writeFully(androidId.md5())
     } shouldEqualsTo 16
 }
 
@@ -571,7 +573,7 @@ internal fun BytePacketBuilder.t187(
 ) {
     writeShort(0x187)
     writeShortLVPacket {
-        writeFully(MiraiPlatformUtils.md5(macAddress)) // may be md5
+        writeFully(macAddress.md5()) // may be md5
     }
 }
 
@@ -581,7 +583,7 @@ internal fun BytePacketBuilder.t188(
 ) {
     writeShort(0x188)
     writeShortLVPacket {
-        writeFully(MiraiPlatformUtils.md5(androidId))
+        writeFully(androidId.md5())
     } shouldEqualsTo 16
 }
 

+ 4 - 3
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt

@@ -26,17 +26,18 @@ import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
 import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
 import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
 import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
 import net.mamoe.mirai.message.data.ForwardMessage
 import net.mamoe.mirai.message.data.asMessageChain
+import net.mamoe.mirai.utils.gzip
+import net.mamoe.mirai.utils.md5
 
 internal class MessageValidationData(
     val data: ByteArray,
-    val md5: ByteArray = MiraiPlatformUtils.md5(data)
+    val md5: ByteArray = data.md5()
 ) {
     override fun toString(): String {
         return "MessageValidationData(data=<size=${data.size}>, md5=${md5.contentToString()})"
@@ -88,7 +89,7 @@ internal fun Collection<ForwardMessage.INode>.calculateValidationDataForGroup(
 
     val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
 
-    return MessageValidationData(MiraiPlatformUtils.gzip(bytes))
+    return MessageValidationData(bytes.gzip())
 }
 
 internal class MultiMsg {

+ 1 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt

@@ -10,7 +10,6 @@
 package net.mamoe.mirai.internal.network.protocol.packet.login
 
 import kotlinx.io.core.ByteReadPacket
-import kotlinx.io.core.use
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.protobuf.ProtoNumber
 import net.mamoe.mirai.event.AbstractEvent
@@ -23,12 +22,12 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPacket
 import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory
 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket
-import net.mamoe.mirai.internal.utils.ByteArrayPool
 import net.mamoe.mirai.internal.utils.hexToBytes
 import net.mamoe.mirai.internal.utils.io.ProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.*
 import net.mamoe.mirai.internal.utils.io.withUse
 import net.mamoe.mirai.internal.utils.toReadPacket
+import net.mamoe.mirai.utils.ByteArrayPool
 import net.mamoe.mirai.utils.verbose
 import net.mamoe.mirai.internal.network.protocol.data.jce.PushReq as PushReqJceStruct
 

+ 7 - 3
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt

@@ -9,6 +9,7 @@
 
 package net.mamoe.mirai.internal.network.protocol.packet.login
 
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.sync.withLock
 import kotlinx.io.core.ByteReadPacket
@@ -30,10 +31,13 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.*
 import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769
 import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline
 import net.mamoe.mirai.internal.network.protocol.packet.*
-import net.mamoe.mirai.internal.utils.*
+import net.mamoe.mirai.internal.utils.NetworkType
+import net.mamoe.mirai.internal.utils._miraiContentToString
+import net.mamoe.mirai.internal.utils.encodeToString
 import net.mamoe.mirai.internal.utils.io.serialization.*
 import net.mamoe.mirai.utils.currentTimeMillis
-import java.util.concurrent.CancellationException
+import net.mamoe.mirai.internal.utils.toReadPacket
+import net.mamoe.mirai.utils.localIpAddress
 
 @Suppress("EnumEntryName", "unused")
 internal enum class RegPushReason {
@@ -157,7 +161,7 @@ internal class StatSvc {
                                 strDevType = client.device.model.encodeToString(),
                                 strOSVer = client.device.version.release.encodeToString(),
                                 uOldSSOIp = 0,
-                                uNewSSOIp = MiraiPlatformUtils.localIpAddress().runCatching { ipToLong() }
+                                uNewSSOIp = localIpAddress().runCatching { ipToLong() }
                                     .getOrElse { "192.168.1.123".ipToLong() },
                                 strVendorName = "MIUI",
                                 strVendorOSName = "?ONEPLUS A5000_23_17",

+ 2 - 1
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt

@@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.utils.io.*
 import net.mamoe.mirai.utils.currentTimeSeconds
 import net.mamoe.mirai.utils.error
 import net.mamoe.mirai.utils.generateDeviceInfoData
+import net.mamoe.mirai.utils.md5
 
 internal class WtLogin {
     /**
@@ -80,7 +81,7 @@ internal class WtLogin {
                         t8(2052)
                         t104(client.t104)
                         t116(client.miscBitMap, client.subSigMap)
-                        t401(MiraiPlatformUtils.md5(client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402))
+                        t401((client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402).md5())
                     }
                 }
             }

+ 0 - 18
mirai-core/src/commonMain/kotlin/utils/ByteArrayPool.kt

@@ -1,18 +0,0 @@
-/*
- * Copyright 2019-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.internal.utils
-
-import kotlinx.io.pool.ObjectPool
-
-/**
- * 缓存 [ByteArray] 实例的 [ObjectPool]
- */
-@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-internal typealias ByteArrayPool = net.mamoe.mirai.utils.internal.ByteArrayPool

+ 0 - 114
mirai-core/src/commonMain/kotlin/utils/MiraiPlatformUtils.kt

@@ -1,114 +0,0 @@
-/*
- * Copyright 2019-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.internal.utils
-
-import io.ktor.client.*
-import io.ktor.client.engine.cio.*
-import io.ktor.util.*
-import kotlinx.io.pool.useInstance
-import java.io.ByteArrayOutputStream
-import java.io.InputStream
-import java.io.OutputStream
-import java.net.Inet4Address
-import java.security.MessageDigest
-import java.util.zip.Deflater
-import java.util.zip.GZIPInputStream
-import java.util.zip.GZIPOutputStream
-import java.util.zip.Inflater
-
-internal object MiraiPlatformUtils {
-    fun unzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): 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()
-        }
-    }
-
-    fun zip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): 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() }
-        }
-    }
-
-    fun gzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray {
-        ByteArrayOutputStream().use { buf ->
-            GZIPOutputStream(buf).use { gzip ->
-                data.inputStream(offset, length).use { t -> t.copyTo(gzip) }
-            }
-            buf.flush()
-            return buf.toByteArray()
-        }
-    }
-
-    fun ungzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray {
-        return GZIPInputStream(data.inputStream(offset, length)).use { it.readBytes() }
-    }
-
-    fun md5(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray {
-        data.checkOffsetAndLength(offset, length)
-        return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()
-    }
-
-    fun md5(str: String): ByteArray = md5(str.toByteArray())
-
-    /**
-     * Ktor HttpClient. 不同平台使用不同引擎.
-     */
-    @OptIn(KtorExperimentalAPI::class)
-    val Http: HttpClient = HttpClient(CIO)
-
-    /**
-     * Localhost 解析
-     */
-    fun localIpAddress(): String = kotlin.runCatching {
-        Inet4Address.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()
-    }
-}
-
-@Suppress("DuplicatedCode") // false positive. `this` is not the same for `List<Byte>` and `ByteArray`
-internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) {
-    require(offset >= 0) { "offset shouldn't be negative: $offset" }
-    require(length >= 0) { "length shouldn't be negative: $length" }
-    require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" }
-}

+ 3 - 6
mirai-core/src/commonMain/kotlin/utils/byteArrays.kt

@@ -17,14 +17,11 @@ import kotlinx.io.charsets.Charset
 import kotlinx.io.charsets.Charsets
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.String
-import kotlinx.io.core.use
+import net.mamoe.mirai.internal.utils.io.withUse
+import net.mamoe.mirai.utils.checkOffsetAndLength
 import java.util.*
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
-import kotlin.jvm.JvmMultifileClass
-import kotlin.jvm.JvmName
-import kotlin.jvm.JvmOverloads
-import kotlin.jvm.JvmSynthetic
 
 
 @JvmOverloads
@@ -107,5 +104,5 @@ internal inline fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
     contract {
         callsInPlace(t, InvocationKind.EXACTLY_ONCE)
     }
-    return this.toReadPacket().use(t)
+    return this.toReadPacket().withUse(t)
 }

+ 1 - 2
mirai-core/src/commonMain/kotlin/utils/crypto/TEA.kt

@@ -10,10 +10,9 @@
 package net.mamoe.mirai.internal.utils.crypto
 
 import kotlinx.io.core.ByteReadPacket
-import kotlinx.io.pool.useInstance
-import net.mamoe.mirai.internal.utils.ByteArrayPool
 import net.mamoe.mirai.internal.utils.toByteArray
 import net.mamoe.mirai.internal.utils.toUHexString
+import net.mamoe.mirai.utils.ByteArrayPool
 import kotlin.experimental.and
 import kotlin.experimental.xor
 import kotlin.random.Random

+ 1 - 4
mirai-core/src/commonMain/kotlin/utils/io/input.kt

@@ -16,14 +16,11 @@ package net.mamoe.mirai.internal.utils.io
 import kotlinx.io.charsets.Charset
 import kotlinx.io.charsets.Charsets
 import kotlinx.io.core.*
-import net.mamoe.mirai.internal.utils.ByteArrayPool
 import net.mamoe.mirai.internal.utils.toReadPacket
 import net.mamoe.mirai.internal.utils.toUHexString
+import net.mamoe.mirai.utils.ByteArrayPool
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
-import kotlin.jvm.JvmMultifileClass
-import kotlin.jvm.JvmName
-import kotlin.jvm.JvmSynthetic
 
 @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
 internal inline fun <R> ByteReadPacket.useBytes(

+ 6 - 2
mirai-core/src/commonTest/kotlin/PlatformUtilsTest.kt

@@ -9,6 +9,10 @@
 package net.mamoe.mirai.internal.utils
 
 import kotlinx.io.core.toByteArray
+import net.mamoe.mirai.utils.gzip
+import net.mamoe.mirai.utils.ungzip
+import net.mamoe.mirai.utils.unzip
+import net.mamoe.mirai.utils.zip
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
@@ -16,11 +20,11 @@ internal class PlatformUtilsTest {
 
     @Test
     fun testZip() {
-        assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString())
+        assertEquals("test", "test".toByteArray().zip().unzip().encodeToString())
     }
 
     @Test
     fun testGZip() {
-        assertEquals("test", MiraiPlatformUtils.ungzip(MiraiPlatformUtils.gzip("test".toByteArray())).encodeToString())
+        assertEquals("test", "test".toByteArray().gzip().ungzip().encodeToString())
     }
 }

+ 2 - 2
mirai-core/src/commonTest/kotlin/test/printing.kt

@@ -14,10 +14,10 @@ package test
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.Input
 import kotlinx.io.core.readAvailable
-import kotlinx.io.pool.useInstance
-import net.mamoe.mirai.internal.utils.ByteArrayPool
+import kotlinx.io.core.use
 import net.mamoe.mirai.internal.utils.toReadPacket
 import net.mamoe.mirai.internal.utils.toUHexString
+import net.mamoe.mirai.utils.ByteArrayPool
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.MiraiLoggerWithSwitch
 import net.mamoe.mirai.utils.withSwitch

+ 2 - 2
mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.internal.utils.crypto
 
-import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
+import net.mamoe.mirai.utils.md5
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import java.security.*
 import java.security.spec.ECGenParameterSpec
@@ -81,7 +81,7 @@ internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
             val instance = KeyAgreement.getInstance("ECDH", "BC")
             instance.init(privateKey)
             instance.doPhase(publicKey, true)
-            return MiraiPlatformUtils.md5(instance.generateSecret())
+            return instance.generateSecret().md5()
         }
 
         actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {