From 13dadd5a9543f41cb794f0ed82509bf423d1da74 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 30 May 2022 21:30:52 +0100 Subject: [PATCH] Implement mirai-core for native --- buildSrc/src/main/kotlin/HmppConfigure.kt | 13 + buildSrc/src/main/kotlin/Versions.kt | 2 +- gradle.properties | 4 +- .../plugin/PluginDataRenameToIdTest.kt | 1 + .../android/api/android.api | 55 +- .../compatibility-validation/jvm/api/jvm.api | 51 +- .../androidTest/kotlin/android/util/Log.kt | 125 ++++ .../src/androidTest/kotlin/package.kt | 10 +- .../kotlin/contact/FileSupported.kt | 15 +- .../kotlin/contact/file/AbsoluteFolder.kt | 7 - .../kotlin/contact/roaming/RoamingMessages.kt | 3 - .../commonMain/kotlin/event/EventChannel.kt | 10 - .../src/commonMain/kotlin/event/Extensions.kt | 1 + .../kotlin/event/MessageSelectBuilderUnit.kt | 214 ++++++ .../commonMain/kotlin/event/events/friend.kt | 2 - .../commonMain/kotlin/event/events/group.kt | 8 +- .../src/commonMain/kotlin/event/select.kt | 290 -------- .../kotlin/message/data/FileMessage.kt | 73 +- .../src/commonMain/kotlin/message/utils.kt | 3 +- .../src/commonMain/kotlin/utils/RemoteFile.kt | 584 ---------------- .../kotlin/contact/FileSupported.kt | 44 ++ .../kotlin/contact/file/AbsoluteFolder.kt | 8 +- .../kotlin/contact/roaming/RoamingMessages.kt | 9 +- .../jvmBaseMain/kotlin/event/EventChannel.kt | 11 + .../kotlin/event/MessageSelectBuilderUnit.kt | 126 ++++ .../kotlin/event/deprecated.nextEvent.kt | 2 - .../kotlin/event/deprecated.nextEventAsync.kt | 2 - .../kotlin/event/deprecated.syncFromEvent.kt | 2 - .../kotlin/message/data/FileMessage.kt | 120 ++++ .../kotlin/utils/BotConfiguration.kt | 37 +- .../jvmBaseMain/kotlin/utils/RemoteFile.kt | 142 ++-- .../kotlin/contact/FileSupported.kt | 33 + .../kotlin/event/MessageSelectBuilderUnit.kt | 25 + .../kotlin/message/data/FileMessage.kt | 110 +++ .../kotlin/utils/BotConfiguration.kt | 7 +- .../nativeMain/kotlin/utils/LoginSolver.kt | 2 +- .../nativeMain/kotlin/utils/MiraiLogger.kt | 4 +- .../src/nativeMain/kotlin/utils/RemoteFile.kt | 575 ---------------- mirai-core-utils/build.gradle.kts | 2 +- .../src/commonMain/kotlin/ByteArrayPool.kt | 4 +- .../src/commonMain/kotlin/Bytes.kt | 8 +- .../src/commonMain/kotlin/Conversions.kt | 53 +- .../src/commonMain/kotlin/File.kt | 2 +- mirai-core-utils/src/commonMain/kotlin/IO.kt | 2 + .../src/mingwMain/kotlin/MiraiFileImpl.kt | 174 ----- .../src/mingwTest/kotlin/MiraiFileImplTest.kt | 35 - .../src/mingwX64Main/kotlin/MiraiFileImpl.kt | 343 ++++++++++ .../kotlin/StandardUtils.kt | 4 +- .../mingwX64Test/kotlin/MiraiFileImplTest.kt | 45 ++ .../src/nativeMain/kotlin/Clock.kt | 4 +- .../nativeMain/kotlin/ExceptionCollector.kt | 9 +- .../src/nativeMain/kotlin/MiraiFile.kt | 2 +- .../src/nativeMain/kotlin/Service.kt | 12 +- .../src/nativeMain/kotlin/TimeUtils.kt | 31 +- .../src/nativeMain/kotlin/getProperty.kt | 6 +- .../kotlin/AbstractNativeMiraiFileImplTest.kt | 75 ++- .../src/nativeTest/kotlin/TimeUtilsTest.kt | 2 +- .../src/unixMain/kotlin/MiraiFileImpl.kt | 4 +- .../unixTest/kotlin/UnixMiraiFileImplTest.kt | 10 + mirai-core/.gitignore | 3 +- mirai-core/build.gradle.kts | 52 ++ .../androidTest/kotlin/android/util/Log.kt | 127 ++++ mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 55 +- .../commonMain/kotlin/contact/GroupImpl.kt | 48 +- .../message/protocol/MessageProtocol.kt | 1 + .../components/EcdhInitialPublicKeyUpdater.kt | 4 +- .../kotlin/network/components/ServerList.kt | 3 +- .../network/handler/CommonNetworkHandler.kt | 72 +- .../network/handler/NetworkHandlerFactory.kt | 13 +- .../handler/state/LoggingStateObserver.kt | 2 + .../network/protocol/packet/login/StatSvc.kt | 2 +- .../kotlin/pipeline/ProcessorPipeline.kt | 1 - .../src/commonMain/kotlin/utils/FileSystem.kt | 49 ++ .../commonMain/kotlin/utils/PlatformSocket.kt | 11 +- .../commonMain/kotlin/utils/RemoteFileImpl.kt | 624 ------------------ .../commonMain/kotlin/utils/crypto/ECDH.kt | 27 +- .../kotlin/event/EventChannelFlowTest.kt | 2 - .../kotlin/event/EventChannelTest.kt | 18 +- .../src/commonTest/kotlin/event/EventTests.kt | 3 +- .../protocol/MessageProtocolFacadeTest.kt | 5 +- .../protocol/impl/QuoteReplyProtocolTest.kt | 3 + .../AbstractMutableComponentStorageTest.kt | 13 + .../network/component/CombinedStorageTest.kt | 15 + .../AbstractRealNetworkHandlerTest.kt | 7 +- .../network/handler/SelectorRecoveryTest.kt | 3 +- .../kotlin/notice/processors/MessageTest.kt | 13 +- .../kotlin/utils/crypto/ECDHTest.kt | 70 ++ mirai-core/src/darwinMain/kotlin/MiraiImpl.kt | 24 + mirai-core/src/darwinMain/kotlin/package.kt | 10 + mirai-core/src/darwinTest/kotlin/package.kt | 10 + .../src/jvmBaseMain/kotlin/MiraiImpl.kt | 33 + .../jvmBaseMain/kotlin/contact/GroupImpl.kt | 34 + .../kotlin/network/handler/SocketAddress.kt | 10 +- .../network/impl/netty/NettyNetworkHandler.kt | 33 +- .../impl/netty/NettyNetworkHandlerFactory.kt | 2 +- .../kotlin/utils/PlatformSocket.kt | 2 +- .../kotlin/utils/RemoteFileImpl.kt | 569 +++++++++++++++- .../jvmBaseMain/kotlin/utils/crypto/ECDH.kt | 8 +- .../network/component/ComponentKeyTest.kt | 0 ...ConcurrentComponentStorageToStringTest.kt} | 23 +- .../StructureToStringTransformerNewTest.kt | 0 .../kotlin/utils/crypto/ECDHJvmDesktop.kt | 6 +- .../kotlin/netinternalkit/NetReplayHelper.kt | 9 +- .../src/linuxX64Main/kotlin/MiraiImpl.kt | 24 + mirai-core/src/linuxX64Main/kotlin/package.kt | 10 + mirai-core/src/linuxX64Test/kotlin/package.kt | 10 + .../src/mingwX64Main/cinterop/Socket.def | 31 + .../src/mingwX64Main/kotlin/MiraiImpl.kt | 24 + mirai-core/src/mingwX64Main/kotlin/package.kt | 10 + .../kotlin/utils/PlatformSocket.kt | 138 ++++ .../src/nativeMain/cinterop/OpenSSL.def | 43 ++ mirai-core/src/nativeMain/kotlin/MiraiImpl.kt | 27 + .../nativeMain/kotlin/contact/GroupImpl.kt | 26 + .../handler/LengthDelimitedPacketReader.kt | 119 ++++ .../network/handler/NativeNetworkHandler.kt | 113 ++++ .../network/handler/NetworkHandlerFactory.kt | 33 + .../kotlin/network/handler/SocketAddress.kt | 42 +- .../kotlin/utils/MiraiCoreServices.kt | 4 + .../nativeMain/kotlin/utils/PlatformSocket.kt | 53 +- .../nativeMain/kotlin/utils/crypto/ECDH.kt | 225 +++++++ .../kotlin/utils/crypto/ECDHPrivateKey.kt | 86 --- .../LengthDelimitedPacketReaderTest.kt | 538 +++++++++++++++ .../network/framework/AbstractCommonNHTest.kt | 20 +- .../kotlin/test/PlatformInitializationTest.kt | 8 + mirai-core/src/unixMain/cinterop/Socket.def | 30 + mirai-core/src/unixMain/kotlin/package.kt | 10 + .../unixMain/kotlin/utils/PlatformSocket.kt | 173 +++++ 127 files changed, 4383 insertions(+), 2990 deletions(-) create mode 100644 mirai-core-api/src/androidTest/kotlin/android/util/Log.kt rename mirai-core/src/nativeMain/kotlin/utils/RemoteFileImpl.kt => mirai-core-api/src/androidTest/kotlin/package.kt (52%) create mode 100644 mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt delete mode 100644 mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt create mode 100644 mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt create mode 100644 mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt rename mirai-core-api/src/{commonMain => jvmBaseMain}/kotlin/event/deprecated.nextEvent.kt (99%) rename mirai-core-api/src/{commonMain => jvmBaseMain}/kotlin/event/deprecated.nextEventAsync.kt (98%) rename mirai-core-api/src/{commonMain => jvmBaseMain}/kotlin/event/deprecated.syncFromEvent.kt (99%) create mode 100644 mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt create mode 100644 mirai-core-api/src/nativeMain/kotlin/contact/FileSupported.kt create mode 100644 mirai-core-api/src/nativeMain/kotlin/event/MessageSelectBuilderUnit.kt create mode 100644 mirai-core-api/src/nativeMain/kotlin/message/data/FileMessage.kt delete mode 100644 mirai-core-api/src/nativeMain/kotlin/utils/RemoteFile.kt delete mode 100644 mirai-core-utils/src/mingwMain/kotlin/MiraiFileImpl.kt delete mode 100644 mirai-core-utils/src/mingwTest/kotlin/MiraiFileImplTest.kt create mode 100644 mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt rename mirai-core-utils/src/{mingwMain => mingwX64Main}/kotlin/StandardUtils.kt (74%) create mode 100644 mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt create mode 100644 mirai-core/src/androidTest/kotlin/android/util/Log.kt create mode 100644 mirai-core/src/commonMain/kotlin/utils/FileSystem.kt delete mode 100644 mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt create mode 100644 mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt create mode 100644 mirai-core/src/darwinMain/kotlin/MiraiImpl.kt create mode 100644 mirai-core/src/darwinMain/kotlin/package.kt create mode 100644 mirai-core/src/darwinTest/kotlin/package.kt create mode 100644 mirai-core/src/jvmBaseMain/kotlin/MiraiImpl.kt create mode 100644 mirai-core/src/jvmBaseMain/kotlin/contact/GroupImpl.kt rename mirai-core/src/{commonTest => jvmBaseTest}/kotlin/network/component/ComponentKeyTest.kt (100%) rename mirai-core/src/{commonTest/kotlin/network/component/ConcurrentComponentStorageTest.kt => jvmBaseTest/kotlin/network/component/ConcurrentComponentStorageToStringTest.kt} (83%) rename mirai-core/src/{commonTest => jvmBaseTest}/kotlin/utils/test/StructureToStringTransformerNewTest.kt (100%) create mode 100644 mirai-core/src/linuxX64Main/kotlin/MiraiImpl.kt create mode 100644 mirai-core/src/linuxX64Main/kotlin/package.kt create mode 100644 mirai-core/src/linuxX64Test/kotlin/package.kt create mode 100644 mirai-core/src/mingwX64Main/cinterop/Socket.def create mode 100644 mirai-core/src/mingwX64Main/kotlin/MiraiImpl.kt create mode 100644 mirai-core/src/mingwX64Main/kotlin/package.kt create mode 100644 mirai-core/src/mingwX64Main/kotlin/utils/PlatformSocket.kt create mode 100644 mirai-core/src/nativeMain/cinterop/OpenSSL.def create mode 100644 mirai-core/src/nativeMain/kotlin/MiraiImpl.kt create mode 100644 mirai-core/src/nativeMain/kotlin/contact/GroupImpl.kt create mode 100644 mirai-core/src/nativeMain/kotlin/network/handler/LengthDelimitedPacketReader.kt create mode 100644 mirai-core/src/nativeMain/kotlin/network/handler/NativeNetworkHandler.kt create mode 100644 mirai-core/src/nativeMain/kotlin/network/handler/NetworkHandlerFactory.kt create mode 100644 mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt delete mode 100644 mirai-core/src/nativeMain/kotlin/utils/crypto/ECDHPrivateKey.kt create mode 100644 mirai-core/src/nativeTest/kotlin/network/LengthDelimitedPacketReaderTest.kt create mode 100644 mirai-core/src/unixMain/cinterop/Socket.def create mode 100644 mirai-core/src/unixMain/kotlin/package.kt create mode 100644 mirai-core/src/unixMain/kotlin/utils/PlatformSocket.kt diff --git a/buildSrc/src/main/kotlin/HmppConfigure.kt b/buildSrc/src/main/kotlin/HmppConfigure.kt index f45e3b903..1ea22acd8 100644 --- a/buildSrc/src/main/kotlin/HmppConfigure.kt +++ b/buildSrc/src/main/kotlin/HmppConfigure.kt @@ -13,6 +13,7 @@ import org.gradle.api.Project import org.gradle.api.attributes.Attribute import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.getting +import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME @@ -85,6 +86,8 @@ val LINUX_TARGETS = setOf("linuxX64") val UNIX_LIKE_TARGETS by lazy { LINUX_TARGETS + MAC_TARGETS } +val NATIVE_TARGETS by lazy { UNIX_LIKE_TARGETS + WIN_TARGETS } + fun Project.configureHMPPJvm() { extensions.getByType(KotlinMultiplatformExtension::class.java).apply { @@ -277,6 +280,16 @@ fun KotlinMultiplatformExtension.configureNativeTargetsHierarchical( } } + // Workaround from https://youtrack.jetbrains.com/issue/KT-52433/KotlinNative-Unable-to-generate-framework-with-Kotlin-1621-and-Xcode-134#focus=Comments-27-6140143.0-0 + project.tasks.withType().configureEach { + val properties = listOf( + "ios_arm32", "watchos_arm32", "watchos_x86" + ).joinToString(separator = ";") { "clangDebugFlags.$it=-Os" } + kotlinOptions.freeCompilerArgs += listOf( + "-Xoverride-konan-properties=$properties" + ) + } + jvmBaseMain.dependsOn(commonMain) jvmBaseTest.dependsOn(commonTest) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index a994a7b70..0b8048f00 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -31,7 +31,7 @@ object Versions { const val coroutines = "1.6.2" const val atomicFU = "0.17.2" const val serialization = "1.3.2" - const val ktor = "1.6.7" + const val ktor = "1.6.8" const val binaryValidator = "0.4.0" diff --git a/gradle.properties b/gradle.properties index 4fb2fcd07..7d6b3453a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,4 +23,6 @@ gnsp.disableApplyOnlyOnRootProjectEnforcement=true mirai.android.target.api.level=24 # Enable if you want to use mavenLocal for both Gradle plugin and project dependencies resolutions. systemProp.use.maven.local=false -org.gradle.caching=true \ No newline at end of file +org.gradle.caching=true +kotlin.native.ignoreIncorrectDependencies=true +kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file diff --git a/mirai-console/backend/integration-test/test/testpoints/plugin/PluginDataRenameToIdTest.kt b/mirai-console/backend/integration-test/test/testpoints/plugin/PluginDataRenameToIdTest.kt index 99d8913da..8a2c9a217 100644 --- a/mirai-console/backend/integration-test/test/testpoints/plugin/PluginDataRenameToIdTest.kt +++ b/mirai-console/backend/integration-test/test/testpoints/plugin/PluginDataRenameToIdTest.kt @@ -46,6 +46,7 @@ internal object PluginDataRenameToIdTest : AbstractTestPointAsPlugin() { test: a """.trimIndent() ) + File("data/PluginDataRenameToIdTest").mkdirs() File("data/PluginDataRenameToIdTest/test.txt").createNewFile() File("data/PluginDataRenameToIdTest/testdata.yml").writeText( """ diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api index da33732d2..03653bd1b 100644 --- a/mirai-core-api/compatibility-validation/android/api/android.api +++ b/mirai-core-api/compatibility-validation/android/api/android.api @@ -1913,41 +1913,23 @@ public abstract class net/mamoe/mirai/event/MessageSelectBuilder : net/mamoe/mir public synthetic fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V } -public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/MessageSubscribersBuilder { +public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/CommonMessageSelectBuilderUnit { public fun (Lnet/mamoe/mirai/event/events/MessageEvent;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V - public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; - public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Void; - public abstract fun default (Lkotlin/jvm/functions/Function3;)V - public final fun defaultQuoteReply (Lkotlin/jvm/functions/Function1;)V - public final fun defaultReply (Lkotlin/jvm/functions/Function1;)V - public final fun invoke-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void; public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V - protected abstract fun obtainCurrentCoroutineScope ()Lkotlinx/coroutines/CoroutineScope; - protected abstract fun obtainCurrentDeferred ()Lkotlinx/coroutines/CompletableDeferred; - public fun quoteReply-8NSq9Eo (JLjava/lang/String;)V - public fun quoteReply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V - public fun quoteReply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void; public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void; public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void; public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V - public fun reply-8NSq9Eo (JLjava/lang/String;)V - public fun reply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V - public fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void; public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void; public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void; public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V - public final fun timeout (JLkotlin/jvm/functions/Function1;)V - public final fun timeout-1WcQj8o (J)J public final synthetic fun timeout-ncvN2qU (J)J - public final fun timeoutException (JLkotlin/jvm/functions/Function0;)V - public static synthetic fun timeoutException$default (Lnet/mamoe/mirai/event/MessageSelectBuilderUnit;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V } public final class net/mamoe/mirai/event/MessageSelectionTimeoutChecker { @@ -3609,48 +3591,24 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl { +public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/DirectSizeAccess, net/mamoe/mirai/message/data/DirectToStringAccess, net/mamoe/mirai/message/data/MessageChain { public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain; - public synthetic fun add (ILjava/lang/Object;)V - public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V - public synthetic fun add (Ljava/lang/Object;)Z - public fun add (Lnet/mamoe/mirai/message/data/SingleMessage;)Z - public fun addAll (ILjava/util/Collection;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public final fun contains (Ljava/lang/Object;)Z public fun contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z public fun containsAll (Ljava/util/Collection;)Z public fun contentToString ()Ljava/lang/String; - public fun equals (Ljava/lang/Object;)Z public synthetic fun get (I)Ljava/lang/Object; public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage; public fun getHasConstrainSingle ()Z public fun getSize ()I - public fun hashCode ()I - public final fun indexOf (Ljava/lang/Object;)I public fun indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I public fun isEmpty ()Z public fun iterator ()Ljava/util/Iterator; - public final fun lastIndexOf (Ljava/lang/Object;)I public fun lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I public fun listIterator ()Ljava/util/ListIterator; public fun listIterator (I)Ljava/util/ListIterator; - public synthetic fun remove (I)Ljava/lang/Object; - public fun remove (I)Lnet/mamoe/mirai/message/data/SingleMessage; - public fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun replaceAll (Ljava/util/function/UnaryOperator;)V - public fun retainAll (Ljava/util/Collection;)Z public fun serializeToMiraiCode ()Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; - public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; - public fun set (ILnet/mamoe/mirai/message/data/SingleMessage;)Lnet/mamoe/mirai/message/data/SingleMessage; - public final fun size ()I - public fun sort (Ljava/util/Comparator;)V public fun subList (II)Ljava/util/List; - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; public fun toString ()Ljava/lang/String; } @@ -5678,6 +5636,8 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException : } public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException { + public synthetic fun (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getCause ()Ljava/lang/Throwable; } public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException { @@ -6188,14 +6148,11 @@ public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/ public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V protected fun error0 (Ljava/lang/String;)V protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)V - public final synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger; public final fun info (Ljava/lang/String;)V public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V protected fun info0 (Ljava/lang/String;)V protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V public fun isEnabled ()Z - public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger; - public final synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V public final fun verbose (Ljava/lang/String;)V public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V protected fun verbose0 (Ljava/lang/String;)V @@ -6466,7 +6423,7 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util public fun error (Ljava/lang/String;)V public fun error (Ljava/lang/String;Ljava/lang/Throwable;)V public fun error (Ljava/lang/Throwable;)V - public synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger; + public fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger; public fun getIdentity ()Ljava/lang/String; public fun info (Ljava/lang/String;)V public fun info (Ljava/lang/String;Ljava/lang/Throwable;)V @@ -6478,7 +6435,7 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util public fun isVerboseEnabled ()Z public fun isWarningEnabled ()Z public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger; - public synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V + public fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V public fun verbose (Ljava/lang/String;)V public fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V public fun verbose (Ljava/lang/Throwable;)V diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api index 128269dc1..28ca081ee 100644 --- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api +++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api @@ -1913,41 +1913,23 @@ public abstract class net/mamoe/mirai/event/MessageSelectBuilder : net/mamoe/mir public synthetic fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V } -public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/MessageSubscribersBuilder { +public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/CommonMessageSelectBuilderUnit { public fun (Lnet/mamoe/mirai/event/events/MessageEvent;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V - public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; - public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Void; - public abstract fun default (Lkotlin/jvm/functions/Function3;)V - public final fun defaultQuoteReply (Lkotlin/jvm/functions/Function1;)V - public final fun defaultReply (Lkotlin/jvm/functions/Function1;)V - public final fun invoke-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void; public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V - protected abstract fun obtainCurrentCoroutineScope ()Lkotlinx/coroutines/CoroutineScope; - protected abstract fun obtainCurrentDeferred ()Lkotlinx/coroutines/CompletableDeferred; - public fun quoteReply-8NSq9Eo (JLjava/lang/String;)V - public fun quoteReply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V - public fun quoteReply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void; public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void; public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void; public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V - public fun reply-8NSq9Eo (JLjava/lang/String;)V - public fun reply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V - public fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void; public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void; public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void; public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V - public final fun timeout (JLkotlin/jvm/functions/Function1;)V - public final fun timeout-1WcQj8o (J)J public final synthetic fun timeout-ncvN2qU (J)J - public final fun timeoutException (JLkotlin/jvm/functions/Function0;)V - public static synthetic fun timeoutException$default (Lnet/mamoe/mirai/event/MessageSelectBuilderUnit;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V } public final class net/mamoe/mirai/event/MessageSelectionTimeoutChecker { @@ -3609,48 +3591,24 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl { +public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/DirectSizeAccess, net/mamoe/mirai/message/data/DirectToStringAccess, net/mamoe/mirai/message/data/MessageChain { public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain; - public synthetic fun add (ILjava/lang/Object;)V - public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V - public synthetic fun add (Ljava/lang/Object;)Z - public fun add (Lnet/mamoe/mirai/message/data/SingleMessage;)Z - public fun addAll (ILjava/util/Collection;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public final fun contains (Ljava/lang/Object;)Z public fun contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z public fun containsAll (Ljava/util/Collection;)Z public fun contentToString ()Ljava/lang/String; - public fun equals (Ljava/lang/Object;)Z public synthetic fun get (I)Ljava/lang/Object; public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage; public fun getHasConstrainSingle ()Z public fun getSize ()I - public fun hashCode ()I - public final fun indexOf (Ljava/lang/Object;)I public fun indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I public fun isEmpty ()Z public fun iterator ()Ljava/util/Iterator; - public final fun lastIndexOf (Ljava/lang/Object;)I public fun lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I public fun listIterator ()Ljava/util/ListIterator; public fun listIterator (I)Ljava/util/ListIterator; - public synthetic fun remove (I)Ljava/lang/Object; - public fun remove (I)Lnet/mamoe/mirai/message/data/SingleMessage; - public fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun replaceAll (Ljava/util/function/UnaryOperator;)V - public fun retainAll (Ljava/util/Collection;)Z public fun serializeToMiraiCode ()Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; - public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; - public fun set (ILnet/mamoe/mirai/message/data/SingleMessage;)Lnet/mamoe/mirai/message/data/SingleMessage; - public final fun size ()I - public fun sort (Ljava/util/Comparator;)V public fun subList (II)Ljava/util/List; - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; public fun toString ()Ljava/lang/String; } @@ -5678,6 +5636,8 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException : } public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException { + public synthetic fun (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getCause ()Ljava/lang/Throwable; } public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException { @@ -6188,14 +6148,11 @@ public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/ public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V protected fun error0 (Ljava/lang/String;)V protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)V - public final synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger; public final fun info (Ljava/lang/String;)V public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V protected fun info0 (Ljava/lang/String;)V protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V public fun isEnabled ()Z - public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger; - public final synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V public final fun verbose (Ljava/lang/String;)V public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V protected fun verbose0 (Ljava/lang/String;)V diff --git a/mirai-core-api/src/androidTest/kotlin/android/util/Log.kt b/mirai-core-api/src/androidTest/kotlin/android/util/Log.kt new file mode 100644 index 000000000..5d9ecbe64 --- /dev/null +++ b/mirai-core-api/src/androidTest/kotlin/android/util/Log.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package android.util + +import net.mamoe.mirai.internal.utils.StdoutLogger + +// Dummy implementation for tests, since we don't have a SDK + +@Suppress("UNUSED_PARAMETER", "unused") +object Log { + const val VERBOSE = 2 + const val DEBUG = 3 + const val INFO = 4 + const val WARN = 5 + const val ERROR = 6 + const val ASSERT = 7 + + private val stdout = StdoutLogger("AndroidLog") + + @JvmStatic + fun v(tag: String?, msg: String?): Int { + stdout.verbose(msg) + return 0 + } + + @JvmStatic + fun v(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.verbose(msg, tr) + return 0 + } + + @JvmStatic + fun d(tag: String?, msg: String?): Int { + stdout.debug(msg, tr) + return 0 + } + + @JvmStatic + fun d(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.debug(msg, tr) + return 0 + } + + @JvmStatic + fun i(tag: String?, msg: String?): Int { + stdout.info(msg, tr) + return 0 + } + + @JvmStatic + fun i(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.info(msg, tr) + return 0 + } + + @JvmStatic + fun w(tag: String?, msg: String?): Int { + stdout.warning(msg, tr) + return 0 + } + + @JvmStatic + fun w(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.warning(msg, tr) + return 0 + } + + @JvmStatic + fun w(tag: String?, tr: Throwable?): Int { + stdout.warning(msg, tr) + return 0 + } + + @JvmStatic + fun e(tag: String?, msg: String?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun e(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun wtf(tag: String?, msg: String?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun wtf(tag: String?, tr: Throwable?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun wtf(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun getStackTraceString(tr: Throwable): String { + return tr.stackTraceToString() + } + + @JvmStatic + fun println(priority: Int, tag: String?, msg: String?): Int { + stdout.info(msg, tr) + return 0 + } + + + private inline val tr get() = null + private inline val msg get() = null +} diff --git a/mirai-core/src/nativeMain/kotlin/utils/RemoteFileImpl.kt b/mirai-core-api/src/androidTest/kotlin/package.kt similarity index 52% rename from mirai-core/src/nativeMain/kotlin/utils/RemoteFileImpl.kt rename to mirai-core-api/src/androidTest/kotlin/package.kt index a598140e8..b321db446 100644 --- a/mirai-core/src/nativeMain/kotlin/utils/RemoteFileImpl.kt +++ b/mirai-core-api/src/androidTest/kotlin/package.kt @@ -7,12 +7,4 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -package net.mamoe.mirai.internal.utils - -import net.mamoe.mirai.contact.Group - -internal actual class RemoteFileImpl actual constructor(contact: Group, path: String) : - CommonRemoteFileImpl(contact, path) { - - actual constructor(contact: Group, parent: String, name: String) : this(contact, FileSystem.normalize(parent, name)) -} \ No newline at end of file +package net.mameo.mirai \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt index 4f6035bbc..1bf1da557 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -11,7 +11,6 @@ package net.mamoe.mirai.contact import net.mamoe.mirai.contact.file.RemoteFiles -import net.mamoe.mirai.utils.DeprecatedSinceMirai import net.mamoe.mirai.utils.NotStableForInheritance /** @@ -24,17 +23,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance * @see RemoteFiles */ @NotStableForInheritance -public interface FileSupported : Contact { - /** - * 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表. - * - * @since 2.5 - */ - @Suppress("DEPRECATION") - @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public val filesRoot: net.mamoe.mirai.utils.RemoteFile - +public expect interface FileSupported : Contact { /** * 获取远程文件列表 (管理器). * diff --git a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt index e3868ffb8..83846ff74 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt @@ -7,18 +7,13 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -@file:JvmBlockingBridge -@file:Suppress("OVERLOADS_INTERFACE") - package net.mamoe.mirai.contact.file import kotlinx.coroutines.flow.Flow -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.contact.PermissionDeniedException import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.ProgressionCallback -import kotlin.jvm.JvmOverloads /** * 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响. @@ -106,7 +101,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { /** * 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找. */ - @JvmOverloads public suspend fun resolveFileById( id: String, deep: Boolean = false @@ -143,7 +137,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { * * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) */ - @JvmOverloads public suspend fun uploadNewFile( filepath: String, content: ExternalResource, diff --git a/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt b/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt index dd5d235be..dcb50507d 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt @@ -7,12 +7,9 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -@file:JvmBlockingBridge - package net.mamoe.mirai.contact.roaming import kotlinx.coroutines.flow.Flow -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource diff --git a/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt b/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt index 6ad63949a..531abe776 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt @@ -266,16 +266,6 @@ public expect abstract class EventChannel @MiraiInternalA */ public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel - /** - * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] - * @see context - * @since 2.12 - */ - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public fun exceptionHandler(coroutineExceptionHandler: Consumer): EventChannel { - return exceptionHandler { coroutineExceptionHandler.accept(it) } - } /** * 将 [coroutineScope] 作为这个 [EventChannel] 的父作用域. diff --git a/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt b/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt index 053eb52f3..8b4f3912e 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt @@ -142,6 +142,7 @@ public suspend inline fun EventChannel<*>.syncFromE } } +// Can't move to JVM, filename clashes /** * @since 2.10 diff --git a/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt b/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt new file mode 100644 index 000000000..1b8711839 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.event + +import kotlinx.coroutines.* +import net.mamoe.mirai.event.events.MessageEvent +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.MessageSource.Key.quote +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.utils.MiraiInternalApi + +/** + * [selectMessagesUnit] 或 [selectMessages] 时的 DSL 构建器. + * + * 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL + * + * @see MessageSubscribersBuilder 查看上层 API + */ +public expect abstract class MessageSelectBuilderUnit @PublishedApi internal constructor( + ownerMessagePacket: M, + stub: Any?, + subscriber: (M.(String) -> Boolean, MessageListener) -> Unit +) : CommonMessageSelectBuilderUnit + +/** + * [MessageSelectBuilderUnit] 的跨平台实现 + */ +@MiraiInternalApi +public abstract class CommonMessageSelectBuilderUnit protected constructor( + private val ownerMessagePacket: M, + stub: Any?, + subscriber: (M.(String) -> Boolean, MessageListener) -> Unit +) : MessageSubscribersBuilder(stub, subscriber) { + /** + * 当其他条件都不满足时的默认处理. + */ + @MessageDsl + public abstract fun default(onEvent: MessageListener) // 需要后置默认监听器 + + @Deprecated("Use `default` instead", level = DeprecationLevel.HIDDEN) + override fun always(onEvent: MessageListener): Nothing = error("prohibited") + + /** + * 限制本次 select 的最长等待时间, 当超时后抛出 [TimeoutCancellationException] + */ + @Suppress("NOTHING_TO_INLINE") + @MessageDsl + public fun timeoutException( + timeoutMillis: Long, + exception: () -> Throwable + ) { + require(timeoutMillis > 0) { "timeoutMillis must be positive" } + obtainCurrentCoroutineScope().launch { + delay(timeoutMillis) + val deferred = obtainCurrentDeferred() ?: return@launch + if (deferred.isActive && !deferred.isCompleted) { + deferred.completeExceptionally(exception()) + } + } + } + + /** + * 限制本次 select 的最长等待时间, 当超时后执行 [block] 以完成 select + */ + @MessageDsl + public fun timeout(timeoutMillis: Long, block: suspend () -> R) { + require(timeoutMillis > 0) { "timeoutMillis must be positive" } + obtainCurrentCoroutineScope().launch { + delay(timeoutMillis) + val deferred = obtainCurrentDeferred() ?: return@launch + if (deferred.isActive && !deferred.isCompleted) { + deferred.complete(block()) + } + } + } + + + /** + * 返回一个限制本次 select 的最长等待时间的 [Deferred] + * + * @see invoke + * @see reply + */ + @MessageDsl + public fun timeout(timeoutMillis: Long): MessageSelectionTimeoutChecker { + require(timeoutMillis > 0) { "timeoutMillis must be positive" } + return MessageSelectionTimeoutChecker(timeoutMillis) + } + + /** + * 返回一个限制本次 select 的最长等待时间的 [Deferred] + * + * @see Deferred.invoke + */ + @Suppress("unused") + public fun MessageSelectionTimeoutChecker.invoke(block: suspend () -> R) { + return timeout(this.timeoutMillis, block) + } + + /** + * 在超时后回复原消息 + * + * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] + * + * @see timeout + * @see quoteReply + */ + @Suppress("unused", "UNCHECKED_CAST") + public open infix fun MessageSelectionTimeoutChecker.reply(block: suspend () -> Any?) { + return timeout(this.timeoutMillis) { + executeAndReply(block) + Unit as R + } + } + + @Suppress("unused", "UNCHECKED_CAST") + public open infix fun MessageSelectionTimeoutChecker.reply(message: Message) { + return timeout(this.timeoutMillis) { + ownerMessagePacket.subject.sendMessage(message) + Unit as R + } + } + + @Suppress("unused", "UNCHECKED_CAST") + public open infix fun MessageSelectionTimeoutChecker.reply(message: String) { + return timeout(this.timeoutMillis) { + ownerMessagePacket.subject.sendMessage(message) + Unit as R + } + } + + /** + * 在超时后引用回复原消息 + * + * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] + * + * @see timeout + * @see reply + */ + @Suppress("unused", "UNCHECKED_CAST") + public open infix fun MessageSelectionTimeoutChecker.quoteReply(block: suspend () -> Any?) { + return timeout(this.timeoutMillis) { + executeAndQuoteReply(block) + Unit as R + } + } + + @Suppress("unused", "UNCHECKED_CAST") + public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: Message) { + return timeout(this.timeoutMillis) { + ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message) + Unit as R + } + } + + @Suppress("unused", "UNCHECKED_CAST") + public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: String) { + return timeout(this.timeoutMillis) { + ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message) + Unit as R + } + } + + /** + * 当其他条件都不满足时回复原消息. + * + * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] + */ + @MessageDsl + public fun defaultReply(block: suspend () -> Any?): Unit = subscriber({ true }, { + this@CommonMessageSelectBuilderUnit.executeAndReply(block) + }) + + + /** + * 当其他条件都不满足时引用回复原消息. + * + * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] + */ + @MessageDsl + public fun defaultQuoteReply(block: suspend () -> Any?): Unit = subscriber({ true }, { + this@CommonMessageSelectBuilderUnit.executeAndQuoteReply(block) + }) + + private suspend inline fun executeAndReply(noinline block: suspend () -> Any?) { + when (val result = block()) { + Unit -> { + + } + is Message -> ownerMessagePacket.subject.sendMessage(result) + else -> ownerMessagePacket.subject.sendMessage(result.toString()) + } + } + + private suspend inline fun executeAndQuoteReply(noinline block: suspend () -> Any?) { + when (val result = block()) { + Unit -> { + + } + is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result) + else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString()) + } + } + + protected abstract fun obtainCurrentCoroutineScope(): CoroutineScope + protected abstract fun obtainCurrentDeferred(): CompletableDeferred? +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt b/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt index daf03073a..42a8d7775 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt @@ -25,7 +25,6 @@ import net.mamoe.mirai.event.AbstractEvent import net.mamoe.mirai.internal.event.VerboseEvent import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.utils.MiraiInternalApi -import kotlin.jvm.JvmField import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -83,7 +82,6 @@ public data class NewFriendRequestEvent @MiraiInternalApi public constructor( */ public val fromNick: String, ) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent { - @JvmField internal val responded: AtomicBoolean = atomic(false) /** diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/group.kt b/mirai-core-api/src/commonMain/kotlin/event/events/group.kt index f4dfebfea..5fdd59800 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/group.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/group.kt @@ -29,7 +29,10 @@ import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.utils.DeprecatedSinceMirai import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiInternalApi -import kotlin.jvm.* +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic /** * 机器人被踢出群或在其他客户端主动退出一个群. 在事件广播前 [Bot.groups] 就已删除这个群. @@ -354,7 +357,6 @@ public data class BotInvitedJoinGroupRequestEvent @MiraiInternalApi constructor( */ public val invitor: Friend? get() = this.bot.getFriend(invitorId) - @JvmField internal val responded: AtomicBoolean = atomic(false) @JvmBlockingBridge @@ -403,8 +405,6 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor( */ public val invitor: NormalMember? by lazy { invitorId?.let { group?.get(it) } } - @JvmField - @PublishedApi internal val responded: AtomicBoolean = atomic(false) /** diff --git a/mirai-core-api/src/commonMain/kotlin/event/select.kt b/mirai-core-api/src/commonMain/kotlin/event/select.kt index ce8c7b56e..6dbe07fd4 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/select.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/select.kt @@ -14,8 +14,6 @@ package net.mamoe.mirai.event import kotlinx.coroutines.* import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.MessageSource.Key.quote -import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.isContextIdenticalWith import net.mamoe.mirai.message.nextMessage import net.mamoe.mirai.utils.MiraiExperimentalApi @@ -207,294 +205,6 @@ public abstract class MessageSelectBuilder @PublishedApi in override fun ListeningFilter.quoteReply(replier: suspend M.(String) -> Any?): Nothing = error("prohibited") } -/** - * [selectMessagesUnit] 或 [selectMessages] 时的 DSL 构建器. - * - * 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL - * - * @see MessageSubscribersBuilder 查看上层 API - */ -public abstract class MessageSelectBuilderUnit @PublishedApi internal constructor( - private val ownerMessagePacket: M, - stub: Any?, - subscriber: (M.(String) -> Boolean, MessageListener) -> Unit -) : MessageSubscribersBuilder(stub, subscriber) { - /** - * 当其他条件都不满足时的默认处理. - */ - @MessageDsl - public abstract fun default(onEvent: MessageListener) // 需要后置默认监听器 - - @Deprecated("Use `default` instead", level = DeprecationLevel.HIDDEN) - override fun always(onEvent: MessageListener): Nothing = error("prohibited") - - /** - * 限制本次 select 的最长等待时间, 当超时后抛出 [TimeoutCancellationException] - */ - @Suppress("NOTHING_TO_INLINE") - @MessageDsl - public fun timeoutException( - timeoutMillis: Long, - exception: () -> Throwable = { throw MessageSelectionTimeoutException() } - ) { - require(timeoutMillis > 0) { "timeoutMillis must be positive" } - obtainCurrentCoroutineScope().launch { - delay(timeoutMillis) - val deferred = obtainCurrentDeferred() ?: return@launch - if (deferred.isActive && !deferred.isCompleted) { - deferred.completeExceptionally(exception()) - } - } - } - - /** - * 限制本次 select 的最长等待时间, 当超时后执行 [block] 以完成 select - */ - @MessageDsl - public fun timeout(timeoutMillis: Long, block: suspend () -> R) { - require(timeoutMillis > 0) { "timeoutMillis must be positive" } - obtainCurrentCoroutineScope().launch { - delay(timeoutMillis) - val deferred = obtainCurrentDeferred() ?: return@launch - if (deferred.isActive && !deferred.isCompleted) { - deferred.complete(block()) - } - } - } - - - /** - * 返回一个限制本次 select 的最长等待时间的 [Deferred] - * - * @see invoke - * @see reply - */ - @MessageDsl - public fun timeout(timeoutMillis: Long): MessageSelectionTimeoutChecker { - require(timeoutMillis > 0) { "timeoutMillis must be positive" } - return MessageSelectionTimeoutChecker(timeoutMillis) - } - - /** - * 返回一个限制本次 select 的最长等待时间的 [Deferred] - * - * @see Deferred.invoke - */ - @Suppress("unused") - public fun MessageSelectionTimeoutChecker.invoke(block: suspend () -> R) { - return timeout(this.timeoutMillis, block) - } - - /** - * 在超时后回复原消息 - * - * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] - * - * @see timeout - * @see quoteReply - */ - @Suppress("unused", "UNCHECKED_CAST") - public open infix fun MessageSelectionTimeoutChecker.reply(block: suspend () -> Any?) { - return timeout(this.timeoutMillis) { - executeAndReply(block) - Unit as R - } - } - - @Suppress("unused", "UNCHECKED_CAST") - public open infix fun MessageSelectionTimeoutChecker.reply(message: Message) { - return timeout(this.timeoutMillis) { - ownerMessagePacket.subject.sendMessage(message) - Unit as R - } - } - - @Suppress("unused", "UNCHECKED_CAST") - public open infix fun MessageSelectionTimeoutChecker.reply(message: String) { - return timeout(this.timeoutMillis) { - ownerMessagePacket.subject.sendMessage(message) - Unit as R - } - } - - /** - * 在超时后引用回复原消息 - * - * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] - * - * @see timeout - * @see reply - */ - @Suppress("unused", "UNCHECKED_CAST") - public open infix fun MessageSelectionTimeoutChecker.quoteReply(block: suspend () -> Any?) { - return timeout(this.timeoutMillis) { - executeAndQuoteReply(block) - Unit as R - } - } - - @Suppress("unused", "UNCHECKED_CAST") - public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: Message) { - return timeout(this.timeoutMillis) { - ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message) - Unit as R - } - } - - @Suppress("unused", "UNCHECKED_CAST") - public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: String) { - return timeout(this.timeoutMillis) { - ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message) - Unit as R - } - } - - /** - * 当其他条件都不满足时回复原消息. - * - * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] - */ - @MessageDsl - public fun defaultReply(block: suspend () -> Any?): Unit = subscriber({ true }, { - this@MessageSelectBuilderUnit.executeAndReply(block) - }) - - - /** - * 当其他条件都不满足时引用回复原消息. - * - * 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText] - */ - @MessageDsl - public fun defaultQuoteReply(block: suspend () -> Any?): Unit = subscriber({ true }, { - this@MessageSelectBuilderUnit.executeAndQuoteReply(block) - }) - - private suspend inline fun executeAndReply(noinline block: suspend () -> Any?) { - when (val result = block()) { - Unit -> { - - } - is Message -> ownerMessagePacket.subject.sendMessage(result) - else -> ownerMessagePacket.subject.sendMessage(result.toString()) - } - } - - private suspend inline fun executeAndQuoteReply(noinline block: suspend () -> Any?) { - when (val result = block()) { - Unit -> { - - } - is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result) - else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString()) - } - } - - - @JvmName("timeout-ncvN2qU") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker { - return timeout(timeoutMillis) - } - - @Suppress("unused") - @JvmName("invoke-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) { - return invoke(block) - } - - @Suppress("unused") - @JvmName("invoke-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? { - invoke(block) - return null - } - - @JvmName("reply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) { - return reply(block) - } - - @JvmName("reply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? { - reply(block) - return null - } - - @JvmName("reply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply00(message: String) { - return reply(message) - } - - @JvmName("reply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? { - reply(message) - return null - } - - @JvmName("reply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) { - return reply(message) - } - - @JvmName("reply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? { - reply(message) - return null - } - - - @JvmName("quoteReply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) { - return reply(block) - } - - @JvmName("quoteReply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? { - reply(block) - return null - } - - @JvmName("quoteReply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) { - return reply(message) - } - - @JvmName("quoteReply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? { - reply(message) - return null - } - - @JvmName("quoteReply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) { - return reply(message) - } - - @JvmName("quoteReply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? { - reply(message) - return null - } - - protected abstract fun obtainCurrentCoroutineScope(): CoroutineScope - protected abstract fun obtainCurrentDeferred(): CompletableDeferred? -} - @JvmInline @Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") public value class MessageSelectionTimeoutChecker internal constructor(public val timeoutMillis: Long) diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt index 8e30fa56b..87c6f29bd 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt @@ -22,9 +22,11 @@ import net.mamoe.mirai.contact.FileSupported import net.mamoe.mirai.contact.file.AbsoluteFile import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.message.code.CodableMessage -import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode import net.mamoe.mirai.message.data.visitor.MessageVisitor -import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.MiraiInternalApi +import net.mamoe.mirai.utils.NotStableForInheritance +import net.mamoe.mirai.utils.copy +import net.mamoe.mirai.utils.map import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic @@ -46,10 +48,11 @@ import kotlin.jvm.JvmSynthetic * @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定. */ @Serializable(FileMessage.Serializer::class) +@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST") @SerialName(FileMessage.SERIAL_NAME) @NotStableForInheritance @JvmBlockingBridge -public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { +public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { /** * 服务器需要的某种 ID. */ @@ -70,76 +73,58 @@ public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { */ public val size: Long - override fun contentToString(): String = "[文件]$name" // orthodox + open override fun contentToString(): String - override fun appendMiraiCodeTo(builder: StringBuilder) { - builder.append("[mirai:file:") - builder.appendStringAsMiraiCode(id).append(",") - builder.append(internalId).append(",") - builder.appendStringAsMiraiCode(name).append(",") - builder.append(size).append("]") - } + open override fun appendMiraiCodeTo(builder: StringBuilder) /** - * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. - */ - @Suppress("DEPRECATION") - @Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? { - @Suppress("DEPRECATION") - return contact.filesRoot.resolveById(id) - } - - /** - * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. + * 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`. * * @since 2.8 */ public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? - override val key: Key get() = Key + open override val key: Key @MiraiInternalApi - override fun accept(visitor: MessageVisitor, data: D): R { - return visitor.visitFileMessage(this, data) - } + open override fun accept(visitor: MessageVisitor, data: D): R /** * 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更. */ public companion object Key : - AbstractPolymorphicMessageKey( - MessageContent, { it.safeCast() }) { + AbstractPolymorphicMessageKey { - public const val SERIAL_NAME: String = "FileMessage" + @Suppress("CONST_VAL_WITHOUT_INITIALIZER") + public const val SERIAL_NAME: String /** * 构造 [FileMessage] * @since 2.5 */ @JvmStatic - public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage = - Mirai.createFileMessage(id, internalId, name, size) + public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage } - public object Serializer : KSerializer by FallbackSerializer(SERIAL_NAME) // not polymorphic + public object Serializer : KSerializer // not polymorphic +} - @MiraiInternalApi - private open class FallbackSerializer(serialName: String) : KSerializer by Delegate.serializer().map( +@MiraiInternalApi +internal open class FallbackFileMessageSerializer constructor(serialName: String) : + KSerializer by Delegate.serializer().map( Delegate.serializer().descriptor.copy(serialName), serialize = { Delegate(id, internalId, name, size) }, deserialize = { Mirai.createFileMessage(id, internalId, name, size) }, ) { - @SerialName(SERIAL_NAME) - @Serializable - data class Delegate( - val id: String, - val internalId: Int, - val name: String, - val size: Long, - ) - } + @Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST") + @SerialName(FileMessage.SERIAL_NAME) + @Serializable + data class Delegate constructor( + val id: String, + val internalId: Int, + val name: String, + val size: Long, + ) } /** diff --git a/mirai-core-api/src/commonMain/kotlin/message/utils.kt b/mirai-core-api/src/commonMain/kotlin/message/utils.kt index a84db391b..68f1153d7 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/utils.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/utils.kt @@ -21,7 +21,6 @@ import net.mamoe.mirai.event.EventPriority import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.syncFromEvent -import net.mamoe.mirai.event.syncFromEventOrNull import net.mamoe.mirai.message.data.MessageChain import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -74,7 +73,7 @@ public suspend inline fun P.nextMessage( * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [syncFromEvent] 会返回这个值 * @return 消息链. 超时时返回 `null` * - * @see syncFromEventOrNull 实现原理 + * @see syncFromEvent */ @JvmSynthetic public suspend inline fun P.nextMessageOrNull( diff --git a/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt deleted file mode 100644 index 44924444b..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt +++ /dev/null @@ -1,584 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -@file:Suppress("unused", "DEPRECATION") - -package net.mamoe.mirai.utils - -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.FileSupported -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.FileMessage -import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile -import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback -import kotlin.jvm.JvmOverloads -import kotlin.jvm.JvmStatic - -/** - * 表示一个远程文件或目录. - * - * [RemoteFile] 仅保存 [id], [name], [path], [parent], [contact] 这五个属性, 除获取这些属性外的所有的操作都是在*远程*完成的. - * 意味着操作的结果会因文件或目录在服务器中的状态变化而变化. - * - * 与 [File] 类似, [RemoteFile] 是不可变的. [renameTo] 和 [copyTo] 会操作远程文件, 但不会修改当前 [RemoteFile.path] 等属性. - * - * ## 文件操作 - * - * 所有文件操作都在 [RemoteFile] 对象中完成. 可通过 [FileSupported.filesRoot] 获取到表示根目录路径的 [RemoteFile], 并通过 [resolve] 获取到其内文件. - * - * 示例: - * ``` - * val file1: RemoteFile = group.filesRoot.resolve("/foo.txt") // 获取表示群文件 "foo.txt" 的 RemoteFile 实例 - * val file2: RemoteFile = group.filesRoot.resolve("/dir/foo.txt") // 获取表示群文件目录 "dir" 中的 "foo.txt" 的 RemoteFile 实例 - * - * - * val downloadInfo = file1.getDownloadInfo() // 获取该文件的下载方式, 可以自行下载 - * - * - * val message: FileMessage = file2.upload(resource) // 向路径 "/dir/foo.txt" 上传一个文件, 返回可以发送到群内的文件消息. - * group.sendMessage(message) // 发送文件消息到群, 用户才会收到机器人上传文件的提醒. 可以多次发送. - * - * file2.uploadAndSend(resource) // 上传文件并发送文件消息. 是上面两行的简单版本. - * - * - * // 要直接上传文件, 也可以简单地使用任一: - * group.uploadFile("/foo.txt", resource) // Kotlin - * resource.uploadAsFileTo(group, "/foo.txt") // Kotlin - * FileSupported.uploadFile(group, "/foo.txt", resource"); // Java - * ExternalResource.uploadAsFile(resource, group, "/foo.txt") // Java - * ``` - * - * ## 目录操作 - * [RemoteFile] 类似于 [java.io.File], 也可以表示一个目录. - * ``` - * val dir: RemoteFile = group.filesRoot.resolve("/foo") // 获取表示目录 "foo" 的 RemoteFile 实例 - * - * if (dir.exists()) { // 判断目录是否存在 - * // ... - * } - * - * dir.listFiles() // Kotlin 使用, 获取该目录中的文件列表. - * dir.listFilesIterator() // Java 使用, 获取该目录中的文件列表. - * ``` - * - * 注意, 服务器目前只支持一层目录. 即只能存在 "/foo.txt" 和 "/xxx/foo.txt", 而 "/xxx/xxx/foo.txt" 不受支持. - * - * ## 文件名和目录名可重复 - * - * 服务器允许相同名称的文件或目录存在, 这就导致 "/foo" 可能表示多个重名文件中的一个, 也可能表示一个目录. 依靠路径的判断因此不可靠. - * - * 这个特性带来的行为有: - * - [`FileSupported.uploadFile`][uploadFile] 总是往一个路径上传文件, 如果有同名文件存在, 不会覆盖, 而是再创建一个同名文件. - * - [delete] 可能会删除重名文件中的任何一个, 也可能会删除一个目录, 操作顺序取决于服务器. - * - * 为了解决这个问题, [RemoteFile] 可以拥有一个由服务器分配的固定的唯一识别号 [RemoteFile.id]. - * - * 通过 [listFiles] 获取到的 [RemoteFile] 都拥有非 `null` 的 [id]. - * 服务器可以通过 [id] 准确定位重名文件中的某一个. - * 对这样的文件进行 [upload] 时将会覆盖目标文件 (如果存在), 进行 [delete] 时也只会准确操作目标文件. - * - * 只要文件内容无变化, 文件的 [id] 就不会变更. 可以保存 [RemoteFile.id] 并在以后通过 [RemoteFile.resolveById] 准确获取一个目标文件. - * - * @suppress 使用 [RemoteFile] 是稳定的, 但不应该自行实现这个接口. - * @see FileSupported - * @since 2.5 - */ -@Deprecated( - "Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files", - level = DeprecationLevel.WARNING -) // deprecated since 2.8.0-RC -@DeprecatedSinceMirai(warningSince = "2.8") -@NotStableForInheritance -public expect interface RemoteFile { - /** - * 文件名或目录名. - */ - public val name: String - - /** - * 文件的 ID. 群文件允许重名, ID 非空时用来区分重名. - */ - public val id: String? - - /** - * 标准的绝对路径, 起始字符为 '/'. 如 `/foo/bar.txt`. - * - * 根目录路径为 [ROOT_PATH] - */ - public val path: String - - /** - * 获取父目录, 当 [RemoteFile] 表示根目录时返回 `null` - */ - public val parent: RemoteFile? - - /** - * 此文件所属的群或好友 - */ - public val contact: FileSupported - - /** - * 当 [RemoteFile] 表示一个文件时返回 `true`. - */ - public suspend fun isFile(): Boolean - - /** - * 当 [RemoteFile] 表示一个目录时返回 `true`. - */ - public open suspend fun isDirectory(): Boolean - - /** - * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定. - */ - public suspend fun length(): Long - - public class FileInfo @MiraiInternalApi constructor( - name: String, - id: String, - path: String, - length: Long, - downloadTimes: Int, - uploaderId: Long, - uploadTime: Long, - lastModifyTime: Long, - sha1: ByteArray, - md5: ByteArray, - ) { - - /** - * 文件或目录名. - */ - public val name: String - - /** - * 唯一识别标识. - */ - public val id: String - - /** - * 标准绝对路径. - */ - public val path: String - - /** - * 文件长度 (大小) bytes, 目录的 [length] 为 0. - */ - public val length: Long - - /** - * 下载次数. 目录没有下载次数, 此属性总是 `0`. - */ - public val downloadTimes: Int - - /** - * 上传者 ID. 目录没有上传者, 此属性总是 `0`. - */ - public val uploaderId: Long - - /** - * 上传的时间. 目录没有上传时间, 此属性总是 `0`. - */ - public val uploadTime: Long - - /** - * 上次修改时间. 时间戳秒. - */ - public val lastModifyTime: Long - public val sha1: ByteArray - public val md5: ByteArray - - /** - * 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile]. - */ - public suspend fun resolveToFile(contact: FileSupported): RemoteFile - } - - /** - * 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`. - */ - public suspend fun getInfo(): FileInfo? - - /** - * 当文件或目录存在时返回 `true`. - */ - public suspend fun exists(): Boolean - - /** - * @return [path] - */ - public override fun toString(): String - - /////////////////////////////////////////////////////////////////////////// - // resolve - /////////////////////////////////////////////////////////////////////////// - - /** - * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. - * - * @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolve stdlib 内的类似函数 - */ - public fun resolve(relative: String): RemoteFile - - /** - * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同. - * - * @param relative 相对路径. 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolve stdlib 内的类似函数 - */ - public fun resolve(relative: RemoteFile): RemoteFile - - /** - * 获取该目录下的 ID 为 [id] 的文件, 当 [deep] 为 `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`. - * @see resolve - */ - public suspend fun resolveById(id: String, deep: Boolean = true): RemoteFile? - - /** - * 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null` - * @see resolve - */ - public open suspend fun resolveById(id: String): RemoteFile? - - /** - * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`. - * 不会检查 [RemoteFile] 是否表示一个目录. - * - * @param relative 当初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolveSibling stdlib 内的类似函数 - */ - public fun resolveSibling(relative: String): RemoteFile - - /** - * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`. - * 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同. - * - * @param relative 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolveSibling stdlib 内的类似函数 - */ - public fun resolveSibling(relative: RemoteFile): RemoteFile - - /////////////////////////////////////////////////////////////////////////// - // operations - /////////////////////////////////////////////////////////////////////////// - - /** - * 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. - */ - public suspend fun delete(): Boolean - - /** - * 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值. - * 操作非 Bot 自己上传的文件时需要管理员权限. - * - * [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. - */ - public suspend fun renameTo(name: String): Boolean - - /** - * 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. - * - * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. - * - * **注意**: 与 [java.io.File] 类似, 这是将当前 [RemoteFile] 移动到作为 [target], 而不是移动成为 [target] 的子文件或目录. 例如: - * ``` - * val root = group.filesRoot - * root.resolve("test.txt").moveTo(root) // 错误! 这是在将该文件的路径 "test.txt" 修改为 “/” , 而不是修改为 "/test.txt" - * root.resolve("test.txt").moveTo(root.resolve("/")) // 错误! 与上一行相同. - - * root.resolve("/test.txt").moveTo(root.resolve("/test2.txt")) // 正确. 将该文件的路径 "/test.txt" 修改为 “/test2.txt”,相当于重命名文件 - * ``` - * - * @param target 目标文件位置. - */ - public suspend fun moveTo(target: RemoteFile): Boolean - - /** - * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. - * - * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. - * - * **已弃用:** 当 [path] 是绝对路径时, 这个函数运行正常; - * 当它是相对路径时, 将会尝试把当前文件移动到 [RemoteFile.path] 下的子路径 [path], 因此总是失败. - * - * 使用参数为 [RemoteFile] 的 [moveTo] 代替. - * - * @suppress 在 2.6 弃用. 请使用 [moveTo] - */ - @Deprecated( - "Use moveTo(RemoteFile) instead.", - replaceWith = ReplaceWith("this.moveTo(this.resolveSibling(path))"), - level = DeprecationLevel.ERROR - ) // deprecated since 2.7 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") - public open suspend fun moveTo(path: String): Boolean - - /** - * 创建目录. 目录已经存在或无管理员权限时返回 `false`. - * - * 创建后 [isDirectory] 也不一定会返回 `true`. - * 当 [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] 或 [isDirectory] 结果取决于服务器. - */ - public suspend fun mkdir(): Boolean - - /** - * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow]. - * - * 返回的 [Flow] 是*冷*的, 只会在被需要的时候向服务器查询. - */ - public suspend fun listFiles(): Flow - - /** - * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器. - * @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表. - */ - @JavaFriendlyAPI - public suspend fun listFilesIterator(lazy: Boolean): Iterator - - /** - * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList]. - */ - public open suspend fun listFilesCollection(): List - - /** - * 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`. - */ - public suspend fun toMessage(): FileMessage? - - /////////////////////////////////////////////////////////////////////////// - // upload & download - /////////////////////////////////////////////////////////////////////////// - - /** - * 上传进度回调, 可供前端使用, 以提供进度显示. - * @see asProgressionCallback - */ - @Deprecated( - "Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile", - ReplaceWith("contact.files.uploadNewFile(path, this, callback)"), - level = DeprecationLevel.WARNING - ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public interface ProgressionCallback { - /** - * 当上传开始时调用 - */ - public open fun onBegin(file: RemoteFile, resource: ExternalResource) - - /** - * 每当有进度更新时调用. 此方法可能会同时被多个线程调用. - * - * 提示: 可通过 [ExternalResource.size] 获取文件总大小. - */ - public open fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) - - /** - * 当上传成功时调用 - */ - public open fun onSuccess(file: RemoteFile, resource: ExternalResource) - - /** - * 当上传以异常失败时调用 - */ - public open fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) - - public companion object { - /** - * 将一个 [SendChannel] 作为 [ProgressionCallback] 使用. - * - * 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer]到 [SendChannel] 中. - * 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存. - * - * 若 [closeOnFinish] 为 `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel]. - * - * 使用示例: - * ``` - * val progress = Channel(Channel.BUFFERED) - * - * launch { - * // 每 3 秒发送一次上传进度百分比 - * progress.receiveAsFlow().sample(3.seconds).collect { bytes -> - * group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数 - * } - * } - * - * group.filesRoot.resolve("/foo.txt").upload(resource, progress.asProgressionCallback(true)) - * group.sendMessage("File uploaded successfully.") - * ``` - * - * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作. - */ - @JvmStatic - public fun SendChannel.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback - } - } - - /** - * 上传文件到 [RemoteFile] 表示的路径, 上传过程中调用 [callback] 传递进度. - * - * 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件. - * 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend]. - * - * ## 已弃用 - * - * 使用 [sendFile] 代替. 本函数会上传文件但不会发送文件消息. - * 不发送文件消息就导致其他操作都几乎不能完成, 而且经反馈, 用户通常会忘记后续的 [RemoteFile.toMessage] 操作. - * 本函数造成了很大的不必要的迷惑, 故以既上传又发送消息的, 与官方客户端行为相同的 [sendFile] 代替. - * - * 相关问题: [#1250: 群文件在上传后 toRemoteFile 返回 null](https://github.com/mamoe/mirai/issues/1250) - * - * - * **注意**: [resource] 仅表示资源数据, 而不带有文件名属性. - * 与 [java.io.File] 类似, [upload] 是将 [resource] 上传成为 [this][RemoteFile], 而不是上传成为 [this][RemoteFile] 的子文件. 示例: - * ``` - * group.filesRoot.upload(resource) // 错误! 这是在把资源上传成为根目录. - * group.filesRoot.resolve("/").upload(resource) // 错误! 与上一句相同, 这是在把资源上传成为根目录. - * - * val root = group.filesRoot - * root.resolve("test.txt").upload(resource) // 正确. 把资源上传成为根目录下的 "test.txt". - * root.resolve("/test.txt").upload(resource) // 正确. 与上一句相同, 把资源上传成为根目录下的 "test.txt". - * ``` - * - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @param callback 进度回调 - * @throws IllegalStateException 该文件上传失败或权限不足时抛出 - */ - @Deprecated( - "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public suspend fun upload( - resource: ExternalResource, - callback: ProgressionCallback? = null, - ): FileMessage - - /** - * 上传文件到 [RemoteFile.path] 表示的路径. - * ## 已弃用 - * 阅读 [upload] 获取更多信息 - * @see upload - */ - @Suppress("DEPRECATION_ERROR") - @Deprecated( - "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public open suspend fun upload(resource: ExternalResource): FileMessage - - /** - * 上传文件并发送文件消息. - * - * 若 [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件. - * 即使用 [resolve] 或 [resolveSibling] 获取到的 [RemoteFile] 的 [upload] 总是上传一个新文件, - * 而使用 [resolveById] 或 [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件. - * - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @see upload - */ - @MiraiExperimentalApi - public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt - - /** - * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null` - */ - public suspend fun getDownloadInfo(): DownloadInfo? - - public class DownloadInfo @MiraiInternalApi constructor( - filename: String, - id: String, - path: String, - url: String, - sha1: ByteArray, - md5: ByteArray, - ) { - - /** - * @see RemoteFile.name - */ - public val filename: String - - /** - * @see RemoteFile.id - */ - public val id: String - - /** - * 标准绝对路径 - * @see RemoteFile.path - */ - public val path: String - - /** - * HTTP or HTTPS URL - */ - public val url: String - public val sha1: ByteArray - public val md5: ByteArray - override fun toString(): String - } - - public companion object { - /** - * 根目录路径 - * @see RemoteFile.path - */ - @Suppress("CONST_VAL_WITHOUT_INITIALIZER") // compiler bug - public const val ROOT_PATH: String - - /** - * 上传文件并获取文件消息, 但不发送. - * - * ## 已弃用 - * 在 [upload] 获取更多信息 - * - * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @see RemoteFile.upload - */ - @JvmStatic - @JvmOverloads - @Deprecated( - "Use sendFile instead.", - ReplaceWith( - "this.sendFile(path, resource, callback)", - "net.mamoe.mirai.utils.RemoteFile.Companion.sendFile" - ), - level = DeprecationLevel.ERROR - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public suspend fun FileSupported.uploadFile( - path: String, - resource: ExternalResource, - callback: ProgressionCallback? = null, - ): FileMessage - - /** - * 上传文件并发送文件消息到相关 [FileSupported]. - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @see RemoteFile.uploadAndSend - */ - @JvmStatic - @JvmOverloads - @Deprecated( - "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile", - ReplaceWith("this.files.uploadNewFile(path, resource, callback)"), - level = DeprecationLevel.WARNING - ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public suspend fun C.sendFile( - path: String, - resource: ExternalResource, - callback: ProgressionCallback? = null, - ): MessageReceipt - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt new file mode 100644 index 000000000..bcdf3c433 --- /dev/null +++ b/mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.contact + +import net.mamoe.mirai.contact.file.RemoteFiles +import net.mamoe.mirai.utils.DeprecatedSinceMirai +import net.mamoe.mirai.utils.NotStableForInheritance + + +/** + * 支持文件操作的 [Contact]. 目前仅 [Group]. + * + * 获取文件操作相关示例: [RemoteFiles] + * + * @since 2.5 + * + * @see RemoteFiles + */ +@NotStableForInheritance +public actual interface FileSupported : Contact { + /** + * 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表. + * + * @since 2.5 + */ + @Suppress("DEPRECATION") + @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC + @DeprecatedSinceMirai(warningSince = "2.8") + public val filesRoot: net.mamoe.mirai.utils.RemoteFile + + /** + * 获取远程文件列表 (管理器). + * + * @since 2.8 + */ + public actual val files: RemoteFiles +} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt index 7f159258b..1c214bceb 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt @@ -131,9 +131,11 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder { /** * 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找. */ + @Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI + @JvmOverloads public actual suspend fun resolveFileById( id: String, - deep: Boolean + deep: Boolean = false ): AbsoluteFile? /** @@ -187,10 +189,12 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder { * * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) */ + @Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI + @JvmOverloads public actual suspend fun uploadNewFile( filepath: String, content: ExternalResource, - callback: ProgressionCallback?, + callback: ProgressionCallback? = null, ): AbsoluteFile public actual companion object { diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt index 36e205f97..fc3c85c4e 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt @@ -7,9 +7,12 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:JvmBlockingBridge + package net.mamoe.mirai.contact.roaming import kotlinx.coroutines.flow.Flow +import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource @@ -46,10 +49,11 @@ public actual interface RoamingMessages { * @param timeEnd 结束时间, UTC+8 时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值. * @param filter 过滤器. */ + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI public actual suspend fun getMessagesIn( timeStart: Long, timeEnd: Long, - filter: RoamingMessageFilter? + filter: RoamingMessageFilter? = null ): Flow /** @@ -68,8 +72,9 @@ public actual interface RoamingMessages { * * @param filter 过滤器. */ + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI public actual suspend fun getAllMessages( - filter: RoamingMessageFilter? + filter: RoamingMessageFilter? = null ): Flow = getMessagesIn(0, Long.MAX_VALUE, filter) /** diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt index c987de739..86edd7dbd 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt @@ -309,6 +309,17 @@ public actual abstract class EventChannel @MiraiInternalA }) } + /** + * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] + * @see context + * @since 2.12 + */ + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + public fun exceptionHandler(coroutineExceptionHandler: Consumer): EventChannel { + return exceptionHandler { coroutineExceptionHandler.accept(it) } + } + /** * 将 [coroutineScope] 作为这个 [EventChannel] 的父作用域. * diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt new file mode 100644 index 000000000..d5624dd40 --- /dev/null +++ b/mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.event + +import net.mamoe.mirai.event.events.MessageEvent +import net.mamoe.mirai.message.data.Message + +/** + * [selectMessagesUnit] 或 [selectMessages] 时的 DSL 构建器. + * + * 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL + * + * @see MessageSubscribersBuilder 查看上层 API + */ +public actual abstract class MessageSelectBuilderUnit @PublishedApi internal actual constructor( + ownerMessagePacket: M, + stub: Any?, + subscriber: (M.(String) -> Boolean, MessageListener) -> Unit +) : CommonMessageSelectBuilderUnit(ownerMessagePacket, stub, subscriber) { + @JvmName("timeout-ncvN2qU") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker { + return timeout(timeoutMillis) + } + + @Suppress("unused") + @JvmName("invoke-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) { + return invoke(block) + } + + @Suppress("unused") + @JvmName("invoke-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? { + invoke(block) + return null + } + + @JvmName("reply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) { + return reply(block) + } + + @JvmName("reply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? { + reply(block) + return null + } + + @JvmName("reply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply00(message: String) { + return reply(message) + } + + @JvmName("reply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? { + reply(message) + return null + } + + @JvmName("reply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) { + return reply(message) + } + + @JvmName("reply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? { + reply(message) + return null + } + + + @JvmName("quoteReply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) { + return reply(block) + } + + @JvmName("quoteReply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? { + reply(block) + return null + } + + @JvmName("quoteReply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) { + return reply(message) + } + + @JvmName("quoteReply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? { + reply(message) + return null + } + + @JvmName("quoteReply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) { + return reply(message) + } + + @JvmName("quoteReply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? { + reply(message) + return null + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEvent.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEvent.kt similarity index 99% rename from mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEvent.kt rename to mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEvent.kt index fc28c5e13..11b21decc 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEvent.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEvent.kt @@ -17,8 +17,6 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.utils.DeprecatedSinceMirai import kotlin.coroutines.resume -import kotlin.jvm.JvmName -import kotlin.jvm.JvmSynthetic import kotlin.reflect.KClass diff --git a/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEventAsync.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEventAsync.kt similarity index 98% rename from mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEventAsync.kt rename to mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEventAsync.kt index 53af8dbc4..afb522632 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEventAsync.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEventAsync.kt @@ -17,8 +17,6 @@ import net.mamoe.mirai.utils.DeprecatedSinceMirai import net.mamoe.mirai.utils.MiraiExperimentalApi import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlin.jvm.JvmName -import kotlin.jvm.JvmSynthetic /** diff --git a/mirai-core-api/src/commonMain/kotlin/event/deprecated.syncFromEvent.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.syncFromEvent.kt similarity index 99% rename from mirai-core-api/src/commonMain/kotlin/event/deprecated.syncFromEvent.kt rename to mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.syncFromEvent.kt index a3b39a731..dcbd18927 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/deprecated.syncFromEvent.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.syncFromEvent.kt @@ -16,8 +16,6 @@ import kotlinx.coroutines.* import net.mamoe.mirai.utils.DeprecatedSinceMirai import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlin.jvm.JvmName -import kotlin.jvm.JvmSynthetic import kotlin.reflect.KClass /** diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt new file mode 100644 index 000000000..95b08b567 --- /dev/null +++ b/mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.message.data + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.event.events.MessageEvent +import net.mamoe.mirai.message.code.CodableMessage +import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode +import net.mamoe.mirai.message.data.visitor.MessageVisitor +import net.mamoe.mirai.utils.* + +/** + * 文件消息. + * + * [name] 与 [size] 只供本地使用, 发送消息时只会使用 [id] 和 [internalId]. + * + * 注: [FileMessage] 不可二次发送 + * + * ### 文件操作 + * 要下载这个文件, 可通过 [toAbsoluteFile] 获取到 [AbsoluteFile] 然后操作. + * + * 要获取到 [FileMessage], 可以通过 [MessageEvent.message] 获取, 或通过 [AbsoluteFile.toMessage] 得到. + * + * @since 2.5 + * @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定. + */ +@Serializable(FileMessage.Serializer::class) +@SerialName(FileMessage.SERIAL_NAME) +@NotStableForInheritance +@JvmBlockingBridge +public actual interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { + /** + * 服务器需要的某种 ID. + */ + public actual val id: String + + /** + * 服务器需要的某种 ID. + */ + public actual val internalId: Int + + /** + * 文件名 + */ + public actual val name: String + + /** + * 文件大小 bytes + */ + public actual val size: Long + + actual override fun contentToString(): String = "[文件]$name" // orthodox + + actual override fun appendMiraiCodeTo(builder: StringBuilder) { + builder.append("[mirai:file:") + builder.appendStringAsMiraiCode(id).append(",") + builder.append(internalId).append(",") + builder.appendStringAsMiraiCode(name).append(",") + builder.append(size).append("]") + } + + /** + * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. + */ + @Suppress("DEPRECATION") + @Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC + @DeprecatedSinceMirai(warningSince = "2.8") + public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? { + @Suppress("DEPRECATION") + return contact.filesRoot.resolveById(id) + } + + /** + * 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`. + * + * @since 2.8 + */ + public actual suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? + + actual override val key: Key get() = Key + + @MiraiInternalApi + actual override fun accept(visitor: MessageVisitor, data: D): R { + return visitor.visitFileMessage(this, data) + } + + /** + * 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更. + */ + public actual companion object Key : + AbstractPolymorphicMessageKey( + MessageContent, { it.safeCast() }) { + + public actual const val SERIAL_NAME: String = "FileMessage" + + /** + * 构造 [FileMessage] + * @since 2.5 + */ + @JvmStatic + public actual fun create(id: String, internalId: Int, name: String, size: Long): FileMessage = + Mirai.createFileMessage(id, internalId, name, size) + } + + public actual object Serializer : + KSerializer by FallbackFileMessageSerializer(SERIAL_NAME) // not polymorphic +} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt index 73559091c..2f65e7d0a 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt @@ -60,26 +60,6 @@ public actual open class BotConfiguration { // open for Java */ public var workingDir: File = File(".") - /** - * Json 序列化器, 使用 'kotlinx.serialization' - */ - @MiraiExperimentalApi - @Deprecated( - "Changing serial format is going to be forbidden. Deprecated for removal. ", - level = DeprecationLevel.ERROR - ) - @DeprecatedSinceMirai(errorSince = "2.11") // was experimental - public var json: Json = kotlin.runCatching { - Json { - isLenient = true - ignoreUnknownKeys = true - prettyPrint = true - } - }.getOrElse { - @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions - Json {} - } - /////////////////////////////////////////////////////////////////////////// // Coroutines /////////////////////////////////////////////////////////////////////////// @@ -341,7 +321,7 @@ public actual open class BotConfiguration { // open for Java @ConfigurationDsl public actual fun loadDeviceInfoJson(json: String) { deviceInfo = { - this.json.decodeFromString(DeviceInfo.serializer(), json) + DeviceInfoManager.deserialize(json, Companion.json) } } @@ -603,7 +583,6 @@ public actual open class BotConfiguration { // open for Java // To structural order new.workingDir = workingDir @Suppress("DEPRECATION_ERROR") - new.json = json new.parentCoroutineContext = parentCoroutineContext new.heartbeatPeriodMillis = heartbeatPeriodMillis new.heartbeatTimeoutMillis = heartbeatTimeoutMillis @@ -646,6 +625,20 @@ public actual open class BotConfiguration { // open for Java @JvmStatic public actual val Default: BotConfiguration = BotConfiguration() + /** + * Json 序列化器, 使用 'kotlinx.serialization' + */ + internal val json: Json = kotlin.runCatching { + Json { + isLenient = true + ignoreUnknownKeys = true + prettyPrint = true + } + }.getOrElse { + @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions + Json {} + } + internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo { return { @Suppress("DEPRECATION_ERROR") diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt index 3c8b6f82f..0863912ad 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt @@ -103,106 +103,106 @@ import java.io.File ) // deprecated since 2.8.0-RC @DeprecatedSinceMirai(warningSince = "2.8") @NotStableForInheritance -public actual interface RemoteFile { +public interface RemoteFile { /** * 文件名或目录名. */ - public actual val name: String + public val name: String /** * 文件的 ID. 群文件允许重名, ID 非空时用来区分重名. */ - public actual val id: String? + public val id: String? /** * 标准的绝对路径, 起始字符为 '/'. 如 `/foo/bar.txt`. * * 根目录路径为 [ROOT_PATH] */ - public actual val path: String + public val path: String /** * 获取父目录, 当 [RemoteFile] 表示根目录时返回 `null` */ - public actual val parent: RemoteFile? + public val parent: RemoteFile? /** * 此文件所属的群或好友 */ - public actual val contact: FileSupported + public val contact: FileSupported /** * 当 [RemoteFile] 表示一个文件时返回 `true`. */ - public actual suspend fun isFile(): Boolean + public suspend fun isFile(): Boolean /** * 当 [RemoteFile] 表示一个目录时返回 `true`. */ - public actual suspend fun isDirectory(): Boolean = !isFile() + public suspend fun isDirectory(): Boolean = !isFile() /** * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定. */ - public actual suspend fun length(): Long + public suspend fun length(): Long - public actual class FileInfo @MiraiInternalApi actual constructor( + public class FileInfo @MiraiInternalApi constructor( /** * 文件或目录名. */ - public actual val name: String, + public val name: String, /** * 唯一识别标识. */ - public actual val id: String, + public val id: String, /** * 标准绝对路径. */ - public actual val path: String, + public val path: String, /** * 文件长度 (大小) bytes, 目录的 [length] 为 0. */ - public actual val length: Long, + public val length: Long, /** * 下载次数. 目录没有下载次数, 此属性总是 `0`. */ - public actual val downloadTimes: Int, + public val downloadTimes: Int, /** * 上传者 ID. 目录没有上传者, 此属性总是 `0`. */ - public actual val uploaderId: Long, + public val uploaderId: Long, /** * 上传的时间. 目录没有上传时间, 此属性总是 `0`. */ - public actual val uploadTime: Long, + public val uploadTime: Long, /** * 上次修改时间. 时间戳秒. */ - public actual val lastModifyTime: Long, - public actual val sha1: ByteArray, - public actual val md5: ByteArray, + public val lastModifyTime: Long, + public val sha1: ByteArray, + public val md5: ByteArray, ) { /** * 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile]. */ - public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile = + public suspend fun resolveToFile(contact: FileSupported): RemoteFile = contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path) } /** * 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`. */ - public actual suspend fun getInfo(): FileInfo? + public suspend fun getInfo(): FileInfo? /** * 当文件或目录存在时返回 `true`. */ - public actual suspend fun exists(): Boolean + public suspend fun exists(): Boolean /** * @return [path] */ - public actual override fun toString(): String + public override fun toString(): String /////////////////////////////////////////////////////////////////////////// // resolve @@ -214,7 +214,7 @@ public actual interface RemoteFile { * @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析 * @see File.resolve stdlib 内的类似函数 */ - public actual fun resolve(relative: String): RemoteFile + public fun resolve(relative: String): RemoteFile /** * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同. @@ -222,19 +222,20 @@ public actual interface RemoteFile { * @param relative 相对路径. 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析 * @see File.resolve stdlib 内的类似函数 */ - public actual fun resolve(relative: RemoteFile): RemoteFile + public fun resolve(relative: RemoteFile): RemoteFile /** * 获取该目录下的 ID 为 [id] 的文件, 当 [deep] 为 `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`. * @see resolve */ - public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile? + @Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS") // JVM ABI + public suspend fun resolveById(id: String, deep: Boolean = true): RemoteFile? /** * 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null` * @see resolve */ - public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true) + public suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true) /** * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`. @@ -243,7 +244,7 @@ public actual interface RemoteFile { * @param relative 当初始字符为 '/' 时将作为绝对路径解析 * @see File.resolveSibling stdlib 内的类似函数 */ - public actual fun resolveSibling(relative: String): RemoteFile + public fun resolveSibling(relative: String): RemoteFile /** * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`. @@ -252,7 +253,7 @@ public actual interface RemoteFile { * @param relative 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析 * @see File.resolveSibling stdlib 内的类似函数 */ - public actual fun resolveSibling(relative: RemoteFile): RemoteFile + public fun resolveSibling(relative: RemoteFile): RemoteFile /////////////////////////////////////////////////////////////////////////// // operations @@ -261,7 +262,7 @@ public actual interface RemoteFile { /** * 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. */ - public actual suspend fun delete(): Boolean + public suspend fun delete(): Boolean /** * 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值. @@ -269,7 +270,7 @@ public actual interface RemoteFile { * * [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. */ - public actual suspend fun renameTo(name: String): Boolean + public suspend fun renameTo(name: String): Boolean /** * 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. @@ -287,7 +288,7 @@ public actual interface RemoteFile { * * @param target 目标文件位置. */ - public actual suspend fun moveTo(target: RemoteFile): Boolean + public suspend fun moveTo(target: RemoteFile): Boolean /** * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. @@ -307,7 +308,7 @@ public actual interface RemoteFile { level = DeprecationLevel.ERROR ) // deprecated since 2.7 @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") - public actual suspend fun moveTo(path: String): Boolean { + public suspend fun moveTo(path: String): Boolean { // Impl notes: // if `path` is absolute, this works as intended. // if not, `resolve(path)` will be a child path from this dir and fails always. @@ -320,31 +321,31 @@ public actual interface RemoteFile { * 创建后 [isDirectory] 也不一定会返回 `true`. * 当 [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] 或 [isDirectory] 结果取决于服务器. */ - public actual suspend fun mkdir(): Boolean + public suspend fun mkdir(): Boolean /** * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow]. * * 返回的 [Flow] 是*冷*的, 只会在被需要的时候向服务器查询. */ - public actual suspend fun listFiles(): Flow + public suspend fun listFiles(): Flow /** * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器. * @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表. */ @JavaFriendlyAPI - public actual suspend fun listFilesIterator(lazy: Boolean): Iterator + public suspend fun listFilesIterator(lazy: Boolean): Iterator /** * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList]. */ - public actual suspend fun listFilesCollection(): List = listFiles().toList() + public suspend fun listFilesCollection(): List = listFiles().toList() /** * 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`. */ - public actual suspend fun toMessage(): FileMessage? + public suspend fun toMessage(): FileMessage? /////////////////////////////////////////////////////////////////////////// // upload & download @@ -360,30 +361,30 @@ public actual interface RemoteFile { level = DeprecationLevel.WARNING ) // deprecated since 2.8.0-RC @DeprecatedSinceMirai(warningSince = "2.8") - public actual interface ProgressionCallback { + public interface ProgressionCallback { /** * 当上传开始时调用 */ - public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {} + public fun onBegin(file: RemoteFile, resource: ExternalResource) {} /** * 每当有进度更新时调用. 此方法可能会同时被多个线程调用. * * 提示: 可通过 [ExternalResource.size] 获取文件总大小. */ - public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {} + public fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {} /** * 当上传成功时调用 */ - public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {} + public fun onSuccess(file: RemoteFile, resource: ExternalResource) {} /** * 当上传以异常失败时调用 */ - public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {} + public fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {} - public actual companion object { + public companion object { /** * 将一个 [SendChannel] 作为 [ProgressionCallback] 使用. * @@ -410,7 +411,7 @@ public actual interface RemoteFile { * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作. */ @JvmStatic - public actual fun SendChannel.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback { + public fun SendChannel.asProgressionCallback(closeOnFinish: Boolean = false): ProgressionCallback { return object : ProgressionCallback { override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) { trySend(downloadedSize) @@ -462,9 +463,10 @@ public actual interface RemoteFile { "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR ) // deprecated since 2.7-M1 @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public actual suspend fun upload( + @Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS") + public suspend fun upload( resource: ExternalResource, - callback: ProgressionCallback?, + callback: ProgressionCallback? = null, ): FileMessage /** @@ -478,7 +480,7 @@ public actual interface RemoteFile { "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR ) // deprecated since 2.7-M1 @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null) + public suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null) /** * 上传文件. @@ -520,7 +522,7 @@ public actual interface RemoteFile { * @see upload */ @MiraiExperimentalApi - public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt + public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt /** * 上传文件并发送文件消息. @@ -533,41 +535,41 @@ public actual interface RemoteFile { /** * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null` */ - public actual suspend fun getDownloadInfo(): DownloadInfo? + public suspend fun getDownloadInfo(): DownloadInfo? - public actual class DownloadInfo @MiraiInternalApi actual constructor( + public class DownloadInfo @MiraiInternalApi constructor( /** * @see RemoteFile.name */ - public actual val filename: String, + public val filename: String, /** * @see RemoteFile.id */ - public actual val id: String, + public val id: String, /** * 标准绝对路径 * @see RemoteFile.path */ - public actual val path: String, + public val path: String, /** * HTTP or HTTPS URL */ - public actual val url: String, - public actual val sha1: ByteArray, - public actual val md5: ByteArray, + public val url: String, + public val sha1: ByteArray, + public val md5: ByteArray, ) { - actual override fun toString(): String { + override fun toString(): String { return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " + "md5=${md5.toUHexString("")})" } } - public actual companion object { + public companion object { /** * 根目录路径 * @see RemoteFile.path */ - public actual const val ROOT_PATH: String = "/" + public const val ROOT_PATH: String = "/" /** * 上传文件并获取文件消息, 但不发送. @@ -590,10 +592,11 @@ public actual interface RemoteFile { level = DeprecationLevel.ERROR ) // deprecated since 2.7-M1 @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public actual suspend fun FileSupported.uploadFile( + @Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS") + public suspend fun FileSupported.uploadFile( path: String, resource: ExternalResource, - callback: ProgressionCallback?, + callback: ProgressionCallback? = null, ): FileMessage = @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback) @@ -633,13 +636,14 @@ public actual interface RemoteFile { @Deprecated( "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile", ReplaceWith("this.files.uploadNewFile(path, resource, callback)"), - level = DeprecationLevel.WARNING + level = DeprecationLevel.ERROR ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public actual suspend fun C.sendFile( + @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12") + @Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS") + public suspend fun C.sendFile( path: String, resource: ExternalResource, - callback: ProgressionCallback?, + callback: ProgressionCallback? = null, ): MessageReceipt = @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback).sendTo(this) @@ -653,9 +657,9 @@ public actual interface RemoteFile { @Deprecated( "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile", ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"), - level = DeprecationLevel.WARNING + level = DeprecationLevel.ERROR ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") + @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12") public suspend fun C.sendFile( path: String, file: File, diff --git a/mirai-core-api/src/nativeMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/nativeMain/kotlin/contact/FileSupported.kt new file mode 100644 index 000000000..39744e521 --- /dev/null +++ b/mirai-core-api/src/nativeMain/kotlin/contact/FileSupported.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + + +package net.mamoe.mirai.contact + +import net.mamoe.mirai.contact.file.RemoteFiles +import net.mamoe.mirai.utils.NotStableForInheritance + +/** + * 支持文件操作的 [Contact]. 目前仅 [Group]. + * + * 获取文件操作相关示例: [RemoteFiles] + * + * @since 2.5 + * + * @see RemoteFiles + */ +@NotStableForInheritance +public actual interface FileSupported : Contact { + /** + * 获取远程文件列表 (管理器). + * + * @since 2.8 + */ + public actual val files: RemoteFiles +} \ No newline at end of file diff --git a/mirai-core-api/src/nativeMain/kotlin/event/MessageSelectBuilderUnit.kt b/mirai-core-api/src/nativeMain/kotlin/event/MessageSelectBuilderUnit.kt new file mode 100644 index 000000000..204cf1254 --- /dev/null +++ b/mirai-core-api/src/nativeMain/kotlin/event/MessageSelectBuilderUnit.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.event + +import net.mamoe.mirai.event.events.MessageEvent + +/** + * [selectMessagesUnit] 或 [selectMessages] 时的 DSL 构建器. + * + * 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL + * + * @see MessageSubscribersBuilder 查看上层 API + */ +public actual abstract class MessageSelectBuilderUnit @PublishedApi internal actual constructor( + ownerMessagePacket: M, + stub: Any?, + subscriber: (M.(String) -> Boolean, MessageListener) -> Unit +) : CommonMessageSelectBuilderUnit(ownerMessagePacket, stub, subscriber) \ No newline at end of file diff --git a/mirai-core-api/src/nativeMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/nativeMain/kotlin/message/data/FileMessage.kt new file mode 100644 index 000000000..583b60876 --- /dev/null +++ b/mirai-core-api/src/nativeMain/kotlin/message/data/FileMessage.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.message.data + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.event.events.MessageEvent +import net.mamoe.mirai.message.code.CodableMessage +import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode +import net.mamoe.mirai.message.data.visitor.MessageVisitor +import net.mamoe.mirai.utils.MiraiInternalApi +import net.mamoe.mirai.utils.NotStableForInheritance +import net.mamoe.mirai.utils.safeCast + +/** + * 文件消息. + * + * [name] 与 [size] 只供本地使用, 发送消息时只会使用 [id] 和 [internalId]. + * + * 注: [FileMessage] 不可二次发送 + * + * ### 文件操作 + * 要下载这个文件, 可通过 [toAbsoluteFile] 获取到 [AbsoluteFile] 然后操作. + * + * 要获取到 [FileMessage], 可以通过 [MessageEvent.message] 获取, 或通过 [AbsoluteFile.toMessage] 得到. + * + * @since 2.5 + * @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定. + */ +@Serializable(FileMessage.Serializer::class) +@SerialName(FileMessage.SERIAL_NAME) +@NotStableForInheritance +@JvmBlockingBridge +public actual interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { + /** + * 服务器需要的某种 ID. + */ + public actual val id: String + + /** + * 服务器需要的某种 ID. + */ + public actual val internalId: Int + + /** + * 文件名 + */ + public actual val name: String + + /** + * 文件大小 bytes + */ + public actual val size: Long + + actual override fun contentToString(): String = "[文件]$name" // orthodox + + actual override fun appendMiraiCodeTo(builder: StringBuilder) { + builder.append("[mirai:file:") + builder.appendStringAsMiraiCode(id).append(",") + builder.append(internalId).append(",") + builder.appendStringAsMiraiCode(name).append(",") + builder.append(size).append("]") + } + + /** + * 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`. + * + * @since 2.8 + */ + public actual suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? + + actual override val key: Key get() = Key + + @MiraiInternalApi + actual override fun accept(visitor: MessageVisitor, data: D): R { + return visitor.visitFileMessage(this, data) + } + + /** + * 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更. + */ + public actual companion object Key : + AbstractPolymorphicMessageKey( + MessageContent, { it.safeCast() }) { + + public actual const val SERIAL_NAME: String = "FileMessage" + + /** + * 构造 [FileMessage] + * @since 2.5 + */ + public actual fun create(id: String, internalId: Int, name: String, size: Long): FileMessage = + Mirai.createFileMessage(id, internalId, name, size) + } + + public actual object Serializer : + KSerializer by FallbackFileMessageSerializer(SERIAL_NAME) // not polymorphic +} diff --git a/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt index ade77c7e1..589d385f8 100644 --- a/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt +++ b/mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt @@ -314,7 +314,7 @@ public actual open class BotConfiguration { // open for Java @ConfigurationDsl public actual fun loadDeviceInfoJson(json: String) { deviceInfo = { - Companion.json.decodeFromString(DeviceInfo.serializer(), json) + DeviceInfoManager.deserialize(json, Companion.json) } } @@ -329,7 +329,10 @@ public actual open class BotConfiguration { // open for Java public actual fun fileBasedDeviceInfo(filepath: String) { deviceInfo = { val file = MiraiFile.create(workingDir).resolve(filepath) - Json.decodeFromString(DeviceInfo.serializer(), file.readText()) + if (!file.exists()) { + file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), json)) + } + DeviceInfoManager.deserialize(file.readText(), json) } } diff --git a/mirai-core-api/src/nativeMain/kotlin/utils/LoginSolver.kt b/mirai-core-api/src/nativeMain/kotlin/utils/LoginSolver.kt index 6e7361c6a..6ce474a5c 100644 --- a/mirai-core-api/src/nativeMain/kotlin/utils/LoginSolver.kt +++ b/mirai-core-api/src/nativeMain/kotlin/utils/LoginSolver.kt @@ -75,7 +75,7 @@ public actual abstract class LoginSolver actual constructor() { * @return `SwingSolver` 或 `StandardCharImageLoginSolver` 或 `null` */ public actual val Default: LoginSolver? - get() = TODO("Not yet implemented") + get() = null @Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN) @Suppress("unused") diff --git a/mirai-core-api/src/nativeMain/kotlin/utils/MiraiLogger.kt b/mirai-core-api/src/nativeMain/kotlin/utils/MiraiLogger.kt index 17300135e..b47849252 100644 --- a/mirai-core-api/src/nativeMain/kotlin/utils/MiraiLogger.kt +++ b/mirai-core-api/src/nativeMain/kotlin/utils/MiraiLogger.kt @@ -57,11 +57,11 @@ public actual interface MiraiLogger { public actual companion object INSTANCE : Factory by loadService(Factory::class, fallbackImplementation = { object : Factory { override fun create(requester: KClass<*>): MiraiLogger { - return PlatformLogger(requester.qualifiedName ?: requester.simpleName) + return PlatformLogger(requester.simpleName ?: requester.qualifiedName) } override fun create(requester: KClass<*>, identity: String?): MiraiLogger { - return PlatformLogger(identity) + return PlatformLogger(identity ?: requester.simpleName ?: requester.qualifiedName) } } }) diff --git a/mirai-core-api/src/nativeMain/kotlin/utils/RemoteFile.kt b/mirai-core-api/src/nativeMain/kotlin/utils/RemoteFile.kt deleted file mode 100644 index da8081cde..000000000 --- a/mirai-core-api/src/nativeMain/kotlin/utils/RemoteFile.kt +++ /dev/null @@ -1,575 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -@file:Suppress("unused", "DEPRECATION") - -package net.mamoe.mirai.utils - -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.toList -import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.FileSupported -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.FileMessage -import net.mamoe.mirai.message.data.sendTo -import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile -import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback - -/** - * 表示一个远程文件或目录. - * - * [RemoteFile] 仅保存 [id], [name], [path], [parent], [contact] 这五个属性, 除获取这些属性外的所有的操作都是在*远程*完成的. - * 意味着操作的结果会因文件或目录在服务器中的状态变化而变化. - * - * 与 [File] 类似, [RemoteFile] 是不可变的. [renameTo] 和 [copyTo] 会操作远程文件, 但不会修改当前 [RemoteFile.path] 等属性. - * - * ## 文件操作 - * - * 所有文件操作都在 [RemoteFile] 对象中完成. 可通过 [FileSupported.filesRoot] 获取到表示根目录路径的 [RemoteFile], 并通过 [resolve] 获取到其内文件. - * - * 示例: - * ``` - * val file1: RemoteFile = group.filesRoot.resolve("/foo.txt") // 获取表示群文件 "foo.txt" 的 RemoteFile 实例 - * val file2: RemoteFile = group.filesRoot.resolve("/dir/foo.txt") // 获取表示群文件目录 "dir" 中的 "foo.txt" 的 RemoteFile 实例 - * - * - * val downloadInfo = file1.getDownloadInfo() // 获取该文件的下载方式, 可以自行下载 - * - * - * val message: FileMessage = file2.upload(resource) // 向路径 "/dir/foo.txt" 上传一个文件, 返回可以发送到群内的文件消息. - * group.sendMessage(message) // 发送文件消息到群, 用户才会收到机器人上传文件的提醒. 可以多次发送. - * - * file2.uploadAndSend(resource) // 上传文件并发送文件消息. 是上面两行的简单版本. - * - * - * // 要直接上传文件, 也可以简单地使用任一: - * group.uploadFile("/foo.txt", resource) // Kotlin - * resource.uploadAsFileTo(group, "/foo.txt") // Kotlin - * FileSupported.uploadFile(group, "/foo.txt", resource"); // Java - * ExternalResource.uploadAsFile(resource, group, "/foo.txt") // Java - * ``` - * - * ## 目录操作 - * [RemoteFile] 类似于 [java.io.File], 也可以表示一个目录. - * ``` - * val dir: RemoteFile = group.filesRoot.resolve("/foo") // 获取表示目录 "foo" 的 RemoteFile 实例 - * - * if (dir.exists()) { // 判断目录是否存在 - * // ... - * } - * - * dir.listFiles() // Kotlin 使用, 获取该目录中的文件列表. - * dir.listFilesIterator() // Java 使用, 获取该目录中的文件列表. - * ``` - * - * 注意, 服务器目前只支持一层目录. 即只能存在 "/foo.txt" 和 "/xxx/foo.txt", 而 "/xxx/xxx/foo.txt" 不受支持. - * - * ## 文件名和目录名可重复 - * - * 服务器允许相同名称的文件或目录存在, 这就导致 "/foo" 可能表示多个重名文件中的一个, 也可能表示一个目录. 依靠路径的判断因此不可靠. - * - * 这个特性带来的行为有: - * - [`FileSupported.uploadFile`][uploadFile] 总是往一个路径上传文件, 如果有同名文件存在, 不会覆盖, 而是再创建一个同名文件. - * - [delete] 可能会删除重名文件中的任何一个, 也可能会删除一个目录, 操作顺序取决于服务器. - * - * 为了解决这个问题, [RemoteFile] 可以拥有一个由服务器分配的固定的唯一识别号 [RemoteFile.id]. - * - * 通过 [listFiles] 获取到的 [RemoteFile] 都拥有非 `null` 的 [id]. - * 服务器可以通过 [id] 准确定位重名文件中的某一个. - * 对这样的文件进行 [upload] 时将会覆盖目标文件 (如果存在), 进行 [delete] 时也只会准确操作目标文件. - * - * 只要文件内容无变化, 文件的 [id] 就不会变更. 可以保存 [RemoteFile.id] 并在以后通过 [RemoteFile.resolveById] 准确获取一个目标文件. - * - * @suppress 使用 [RemoteFile] 是稳定的, 但不应该自行实现这个接口. - * @see FileSupported - * @since 2.5 - */ -@Deprecated( - "Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files", - level = DeprecationLevel.WARNING -) // deprecated since 2.8.0-RC -@DeprecatedSinceMirai(warningSince = "2.8") -@NotStableForInheritance -public actual interface RemoteFile { - /** - * 文件名或目录名. - */ - public actual val name: String - - /** - * 文件的 ID. 群文件允许重名, ID 非空时用来区分重名. - */ - public actual val id: String? - - /** - * 标准的绝对路径, 起始字符为 '/'. 如 `/foo/bar.txt`. - * - * 根目录路径为 [ROOT_PATH] - */ - public actual val path: String - - /** - * 获取父目录, 当 [RemoteFile] 表示根目录时返回 `null` - */ - public actual val parent: RemoteFile? - - /** - * 此文件所属的群或好友 - */ - public actual val contact: FileSupported - - /** - * 当 [RemoteFile] 表示一个文件时返回 `true`. - */ - public actual suspend fun isFile(): Boolean - - /** - * 当 [RemoteFile] 表示一个目录时返回 `true`. - */ - public actual suspend fun isDirectory(): Boolean = !isFile() - - /** - * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定. - */ - public actual suspend fun length(): Long - - public actual class FileInfo @MiraiInternalApi actual constructor( - /** - * 文件或目录名. - */ - public actual val name: String, - /** - * 唯一识别标识. - */ - public actual val id: String, - /** - * 标准绝对路径. - */ - public actual val path: String, - /** - * 文件长度 (大小) bytes, 目录的 [length] 为 0. - */ - public actual val length: Long, - /** - * 下载次数. 目录没有下载次数, 此属性总是 `0`. - */ - public actual val downloadTimes: Int, - /** - * 上传者 ID. 目录没有上传者, 此属性总是 `0`. - */ - public actual val uploaderId: Long, - /** - * 上传的时间. 目录没有上传时间, 此属性总是 `0`. - */ - public actual val uploadTime: Long, - /** - * 上次修改时间. 时间戳秒. - */ - public actual val lastModifyTime: Long, - public actual val sha1: ByteArray, - public actual val md5: ByteArray, - ) { - /** - * 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile]. - */ - public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile = - contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path) - } - - /** - * 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`. - */ - public actual suspend fun getInfo(): FileInfo? - - /** - * 当文件或目录存在时返回 `true`. - */ - public actual suspend fun exists(): Boolean - - /** - * @return [path] - */ - public actual override fun toString(): String - - /////////////////////////////////////////////////////////////////////////// - // resolve - /////////////////////////////////////////////////////////////////////////// - - /** - * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. - * - * @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolve stdlib 内的类似函数 - */ - public actual fun resolve(relative: String): RemoteFile - - /** - * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同. - * - * @param relative 相对路径. 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolve stdlib 内的类似函数 - */ - public actual fun resolve(relative: RemoteFile): RemoteFile - - /** - * 获取该目录下的 ID 为 [id] 的文件, 当 [deep] 为 `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`. - * @see resolve - */ - public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile? - - /** - * 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null` - * @see resolve - */ - public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true) - - /** - * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`. - * 不会检查 [RemoteFile] 是否表示一个目录. - * - * @param relative 当初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolveSibling stdlib 内的类似函数 - */ - public actual fun resolveSibling(relative: String): RemoteFile - - /** - * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`. - * 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同. - * - * @param relative 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析 - * @see File.resolveSibling stdlib 内的类似函数 - */ - public actual fun resolveSibling(relative: RemoteFile): RemoteFile - - /////////////////////////////////////////////////////////////////////////// - // operations - /////////////////////////////////////////////////////////////////////////// - - /** - * 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. - */ - public actual suspend fun delete(): Boolean - - /** - * 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值. - * 操作非 Bot 自己上传的文件时需要管理员权限. - * - * [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. - */ - public actual suspend fun renameTo(name: String): Boolean - - /** - * 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. - * - * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. - * - * **注意**: 与 [java.io.File] 类似, 这是将当前 [RemoteFile] 移动到作为 [target], 而不是移动成为 [target] 的子文件或目录. 例如: - * ``` - * val root = group.filesRoot - * root.resolve("test.txt").moveTo(root) // 错误! 这是在将该文件的路径 "test.txt" 修改为 “/” , 而不是修改为 "/test.txt" - * root.resolve("test.txt").moveTo(root.resolve("/")) // 错误! 与上一行相同. - - * root.resolve("/test.txt").moveTo(root.resolve("/test2.txt")) // 正确. 将该文件的路径 "/test.txt" 修改为 “/test2.txt”,相当于重命名文件 - * ``` - * - * @param target 目标文件位置. - */ - public actual suspend fun moveTo(target: RemoteFile): Boolean - - /** - * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. - * - * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path]. - * - * **已弃用:** 当 [path] 是绝对路径时, 这个函数运行正常; - * 当它是相对路径时, 将会尝试把当前文件移动到 [RemoteFile.path] 下的子路径 [path], 因此总是失败. - * - * 使用参数为 [RemoteFile] 的 [moveTo] 代替. - * - * @suppress 在 2.6 弃用. 请使用 [moveTo] - */ - @Deprecated( - "Use moveTo(RemoteFile) instead.", - replaceWith = ReplaceWith("this.moveTo(this.resolveSibling(path))"), - level = DeprecationLevel.ERROR - ) // deprecated since 2.7 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") - public actual suspend fun moveTo(path: String): Boolean { - // Impl notes: - // if `path` is absolute, this works as intended. - // if not, `resolve(path)` will be a child path from this dir and fails always. - return moveTo(resolve(path)) - } - - /** - * 创建目录. 目录已经存在或无管理员权限时返回 `false`. - * - * 创建后 [isDirectory] 也不一定会返回 `true`. - * 当 [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] 或 [isDirectory] 结果取决于服务器. - */ - public actual suspend fun mkdir(): Boolean - - /** - * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow]. - * - * 返回的 [Flow] 是*冷*的, 只会在被需要的时候向服务器查询. - */ - public actual suspend fun listFiles(): Flow - - /** - * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器. - * @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表. - */ - @JavaFriendlyAPI - public actual suspend fun listFilesIterator(lazy: Boolean): Iterator - - /** - * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList]. - */ - public actual suspend fun listFilesCollection(): List = listFiles().toList() - - /** - * 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`. - */ - public actual suspend fun toMessage(): FileMessage? - - /////////////////////////////////////////////////////////////////////////// - // upload & download - /////////////////////////////////////////////////////////////////////////// - - /** - * 上传进度回调, 可供前端使用, 以提供进度显示. - * @see asProgressionCallback - */ - @Deprecated( - "Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile", - ReplaceWith("contact.files.uploadNewFile(path, this, callback)"), - level = DeprecationLevel.WARNING - ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public actual interface ProgressionCallback { - /** - * 当上传开始时调用 - */ - public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {} - - /** - * 每当有进度更新时调用. 此方法可能会同时被多个线程调用. - * - * 提示: 可通过 [ExternalResource.size] 获取文件总大小. - */ - public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {} - - /** - * 当上传成功时调用 - */ - public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {} - - /** - * 当上传以异常失败时调用 - */ - public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {} - - public actual companion object { - /** - * 将一个 [SendChannel] 作为 [ProgressionCallback] 使用. - * - * 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer]到 [SendChannel] 中. - * 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存. - * - * 若 [closeOnFinish] 为 `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel]. - * - * 使用示例: - * ``` - * val progress = Channel(Channel.BUFFERED) - * - * launch { - * // 每 3 秒发送一次上传进度百分比 - * progress.receiveAsFlow().sample(3.seconds).collect { bytes -> - * group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数 - * } - * } - * - * group.filesRoot.resolve("/foo.txt").upload(resource, progress.asProgressionCallback(true)) - * group.sendMessage("File uploaded successfully.") - * ``` - * - * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作. - */ - public actual fun SendChannel.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback { - return object : ProgressionCallback { - override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) { - trySend(downloadedSize) - } - - override fun onSuccess(file: RemoteFile, resource: ExternalResource) { - if (closeOnFinish) this@asProgressionCallback.close() - } - - override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) { - if (closeOnFinish) this@asProgressionCallback.close(exception) - } - } - } - } - } - - /** - * 上传文件到 [RemoteFile] 表示的路径, 上传过程中调用 [callback] 传递进度. - * - * 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件. - * 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend]. - * - * ## 已弃用 - * - * 使用 [sendFile] 代替. 本函数会上传文件但不会发送文件消息. - * 不发送文件消息就导致其他操作都几乎不能完成, 而且经反馈, 用户通常会忘记后续的 [RemoteFile.toMessage] 操作. - * 本函数造成了很大的不必要的迷惑, 故以既上传又发送消息的, 与官方客户端行为相同的 [sendFile] 代替. - * - * 相关问题: [#1250: 群文件在上传后 toRemoteFile 返回 null](https://github.com/mamoe/mirai/issues/1250) - * - * - * **注意**: [resource] 仅表示资源数据, 而不带有文件名属性. - * 与 [java.io.File] 类似, [upload] 是将 [resource] 上传成为 [this][RemoteFile], 而不是上传成为 [this][RemoteFile] 的子文件. 示例: - * ``` - * group.filesRoot.upload(resource) // 错误! 这是在把资源上传成为根目录. - * group.filesRoot.resolve("/").upload(resource) // 错误! 与上一句相同, 这是在把资源上传成为根目录. - * - * val root = group.filesRoot - * root.resolve("test.txt").upload(resource) // 正确. 把资源上传成为根目录下的 "test.txt". - * root.resolve("/test.txt").upload(resource) // 正确. 与上一句相同, 把资源上传成为根目录下的 "test.txt". - * ``` - * - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @param callback 进度回调 - * @throws IllegalStateException 该文件上传失败或权限不足时抛出 - */ - @Deprecated( - "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public actual suspend fun upload( - resource: ExternalResource, - callback: ProgressionCallback?, - ): FileMessage - - /** - * 上传文件到 [RemoteFile.path] 表示的路径. - * ## 已弃用 - * 阅读 [upload] 获取更多信息 - * @see upload - */ - @Suppress("DEPRECATION_ERROR") - @Deprecated( - "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null) - - /** - * 上传文件并发送文件消息. - * - * 若 [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件. - * 即使用 [resolve] 或 [resolveSibling] 获取到的 [RemoteFile] 的 [upload] 总是上传一个新文件, - * 而使用 [resolveById] 或 [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件. - * - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @see upload - */ - @MiraiExperimentalApi - public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt - - /** - * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null` - */ - public actual suspend fun getDownloadInfo(): DownloadInfo? - - public actual class DownloadInfo @MiraiInternalApi actual constructor( - /** - * @see RemoteFile.name - */ - public actual val filename: String, - /** - * @see RemoteFile.id - */ - public actual val id: String, - /** - * 标准绝对路径 - * @see RemoteFile.path - */ - public actual val path: String, - /** - * HTTP or HTTPS URL - */ - public actual val url: String, - public actual val sha1: ByteArray, - public actual val md5: ByteArray, - ) { - actual override fun toString(): String { - return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " + - "md5=${md5.toUHexString("")})" - } - } - - public actual companion object { - /** - * 根目录路径 - * @see RemoteFile.path - */ - public actual const val ROOT_PATH: String = "/" - - /** - * 上传文件并获取文件消息, 但不发送. - * - * ## 已弃用 - * 在 [upload] 获取更多信息 - * - * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @see RemoteFile.upload - */ - @Deprecated( - "Use sendFile instead.", - ReplaceWith( - "this.sendFile(path, resource, callback)", - "net.mamoe.mirai.utils.RemoteFile.Companion.sendFile" - ), - level = DeprecationLevel.ERROR - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally - public actual suspend fun FileSupported.uploadFile( - path: String, - resource: ExternalResource, - callback: ProgressionCallback?, - ): FileMessage = - @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback) - - /** - * 上传文件并发送文件消息到相关 [FileSupported]. - * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. - * @see RemoteFile.uploadAndSend - */ - @Deprecated( - "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile", - ReplaceWith("this.files.uploadNewFile(path, resource, callback)"), - level = DeprecationLevel.WARNING - ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8") - public actual suspend fun C.sendFile( - path: String, - resource: ExternalResource, - callback: ProgressionCallback?, - ): MessageReceipt = - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - this.filesRoot.resolve(path).upload(resource, callback).sendTo(this) - } -} diff --git a/mirai-core-utils/build.gradle.kts b/mirai-core-utils/build.gradle.kts index 43079ebfc..0b17c4c82 100644 --- a/mirai-core-utils/build.gradle.kts +++ b/mirai-core-utils/build.gradle.kts @@ -76,7 +76,7 @@ kotlin { } } - val mingwMain by getting { + val mingwX64Main by getting { dependencies { } } diff --git a/mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt b/mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt index da9c19c00..a3a06c9f7 100644 --- a/mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt +++ b/mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt @@ -14,11 +14,11 @@ import io.ktor.utils.io.pool.* /** * 缓存 [ByteArray] 实例的 [ObjectPool] */ -public object ByteArrayPool : DefaultPool(256) { +public object ByteArrayPool : DefaultPool(128) { /** * 每一个 [ByteArray] 的大小 */ - public const val BUFFER_SIZE: Int = 8192 * 8 + public const val BUFFER_SIZE: Int = 4096 override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE) diff --git a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt index ee46bee34..b820377a7 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt @@ -121,8 +121,12 @@ public fun UByteArray.toUHexString(separator: String = " ", offset: Int = 0, len } } -public inline fun ByteArray.toReadPacket(offset: Int = 0, length: Int = this.size - offset): ByteReadPacket = - ByteReadPacket(this, offset = offset, length = length) +public inline fun ByteArray.toReadPacket( + offset: Int = 0, + length: Int = this.size - offset, + noinline release: (ByteArray) -> Unit = {} +): ByteReadPacket = + ByteReadPacket(this, offset = offset, length = length, block = release) public inline fun ByteArray.read(t: ByteReadPacket.() -> R): R { contract { diff --git a/mirai-core-utils/src/commonMain/kotlin/Conversions.kt b/mirai-core-utils/src/commonMain/kotlin/Conversions.kt index 810b2aa9d..fb0b8ba12 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Conversions.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Conversions.kt @@ -23,7 +23,7 @@ import kotlin.jvm.JvmName */ /** - * 255 -> 00 FF + * Converts a Short to its hex representation in network order (big-endian). */ public fun Short.toByteArray(): ByteArray = with(toInt()) { byteArrayOf( @@ -33,7 +33,7 @@ public fun Short.toByteArray(): ByteArray = with(toInt()) { } /** - * 255 -> 00 00 00 FF + * Converts an Int to its hex representation in network order (big-endian). */ public fun Int.toByteArray(): ByteArray = byteArrayOf( ushr(24).toByte(), @@ -43,7 +43,7 @@ public fun Int.toByteArray(): ByteArray = byteArrayOf( ) /** - * 255 -> 00 00 00 FF + * Converts a Long to its hex representation in network order (big-endian). */ public fun Long.toByteArray(): ByteArray = byteArrayOf( (ushr(56) and 0xFF).toByte(), @@ -56,10 +56,13 @@ public fun Long.toByteArray(): ByteArray = byteArrayOf( (ushr(0) and 0xFF).toByte() ) +/** + * Converts an Int to its hex representation in network order (big-endian). + */ public fun Int.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator) /** - * 255 -> 00 FF + * Converts an UShort to its hex representation in network order (big-endian). */ public fun UShort.toByteArray(): ByteArray = with(toUInt()) { byteArrayOf( @@ -68,22 +71,33 @@ public fun UShort.toByteArray(): ByteArray = with(toUInt()) { ) } +/** + * Converts a Short to its hex representation in network order (big-endian). + */ public fun Short.toUHexString(separator: String = " "): String = this.toUShort().toUHexString(separator) +/** + * Converts an UShort to its hex representation in network order (big-endian). + */ public fun UShort.toUHexString(separator: String = " "): String = this.toInt().shr(8).toUShort().toUByte().toUHexString() + separator + this.toUByte().toUHexString() +/** + * Converts an ULong to its hex representation in network order (big-endian). + */ public fun ULong.toUHexString(separator: String = " "): String = this.toLong().toUHexString(separator) +/** + * Converts a Long to its hex representation in network order (big-endian). + */ public fun Long.toUHexString(separator: String = " "): String = this.ushr(32).toUInt().toUHexString(separator) + separator + this.toUInt().toUHexString(separator) -/** - * 255 -> 00 FF - */ -public fun UByte.toByteArray(): ByteArray = byteArrayOf((this and 255u).toByte()) +/** + * Converts an UByte to its hex representation. + */ public fun UByte.toUHexString(): String = this.toByte().toUHexString() /** @@ -97,7 +111,7 @@ public fun UInt.toByteArray(): ByteArray = byteArrayOf( ) /** - * 转 [ByteArray] 后再转 hex + * Converts an UInt to its hex representation in network order (big-endian). */ public fun UInt.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator) @@ -119,22 +133,23 @@ public fun UByte.fixToUHex(): String = if (this.toInt() in 0..15) "0${this.toString(16).uppercase()}" else this.toString(16).uppercase() /** - * 将 [this] 前 4 个 [Byte] 的 bits 合并为一个 [Int] - * - * 详细解释: - * 一个 [Byte] 有 8 bits - * 一个 [Int] 有 32 bits - * 本函数将 4 个 [Byte] 的 bits 连接得到 [Int] + * Converts 4 bytes to an UInt in network order (big-endian). */ public fun ByteArray.toUInt(): UInt = - (this[0].toUInt().and(255u) shl 24) + (this[1].toUInt().and(255u) shl 16) + (this[2].toUInt() - .and(255u) shl 8) + (this[3].toUInt().and( - 255u - ) shl 0) + (this[0].toUInt().and(255u) shl 24) + .plus(this[1].toUInt().and(255u) shl 16) + .plus(this[2].toUInt().and(255u) shl 8) + .plus(this[3].toUInt().and(255u) shl 0) +/** + * Converts 2 bytes to an UShort in network order (big-endian). + */ public fun ByteArray.toUShort(): UShort = ((this[0].toUInt().and(255u) shl 8) + (this[1].toUInt().and(255u) shl 0)).toUShort() +/** + * Converts 4 bytes to an Int in network order (big-endian). + */ public fun ByteArray.toInt(offset: Int = 0): Int = this[offset + 0].toInt().and(255).shl(24) .plus(this[offset + 1].toInt().and(255).shl(16)) diff --git a/mirai-core-utils/src/commonMain/kotlin/File.kt b/mirai-core-utils/src/commonMain/kotlin/File.kt index 74775a115..859355ab0 100644 --- a/mirai-core-utils/src/commonMain/kotlin/File.kt +++ b/mirai-core-utils/src/commonMain/kotlin/File.kt @@ -76,7 +76,7 @@ public fun MiraiFile.writeText(text: String) { } public fun MiraiFile.readText(): String { - return input().use { it.readText() } + return input().use { it.readAllText() } } public fun MiraiFile.readBytes(): ByteArray { diff --git a/mirai-core-utils/src/commonMain/kotlin/IO.kt b/mirai-core-utils/src/commonMain/kotlin/IO.kt index 39838bfc6..685a3d057 100644 --- a/mirai-core-utils/src/commonMain/kotlin/IO.kt +++ b/mirai-core-utils/src/commonMain/kotlin/IO.kt @@ -133,6 +133,8 @@ public fun Input._readTLVMap( return map } +public fun Input.readAllText(): String = Charsets.UTF_8.newDecoder().decode(this) + public inline fun Input.readString(length: Int, charset: Charset = Charsets.UTF_8): String = String(this.readBytes(length), charset = charset) // stdlib diff --git a/mirai-core-utils/src/mingwMain/kotlin/MiraiFileImpl.kt b/mirai-core-utils/src/mingwMain/kotlin/MiraiFileImpl.kt deleted file mode 100644 index 1c09c4a82..000000000 --- a/mirai-core-utils/src/mingwMain/kotlin/MiraiFileImpl.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -package net.mamoe.mirai.utils - -import io.ktor.utils.io.core.* -import kotlinx.cinterop.* -import platform.posix.PATH_MAX -import platform.posix.fopen -import platform.posix.getcwd -import platform.windows.* - - -internal actual class MiraiFileImpl actual constructor( - // canonical - path: String -) : MiraiFile { - override val path = path.replace("/", "\\") - - actual companion object { - private val ROOT_REGEX = Regex("""^([a-zA-z]+:[/\\])""") - private const val SEPARATOR = '\\' - - @Suppress("UnnecessaryOptInAnnotation") - @OptIn(UnsafeNumber::class) - actual fun getWorkingDir(): MiraiFile { - val path = memScoped { - ByteArray(PATH_MAX).usePinned { - getcwd(it.addressOf(0), it.get().size.convert()) - it.get().toKString() - } - } - return MiraiFile.create(path) - } - } - - override val absolutePath: String = kotlin.run { - val result = ROOT_REGEX.matchEntire(path) ?: return@run path.dropLastWhile { it.isSeparator() } - return@run result.groups.first()!!.value - } - - private fun Char.isSeparator() = this == '/' || this == '\\' - - override val parent: MiraiFile? by lazy { - val absolute = absolutePath - val p = absolute.substringBeforeLast(SEPARATOR, "") - if (p.isEmpty()) { - return@lazy null - } - if (p.lastOrNull() == ':') { - if (absolute.lastIndexOf(SEPARATOR) == p.lastIndex) { - // file is C:/ - return@lazy null - } else { - return@lazy MiraiFileImpl("$p/") // file is C:/xxx - } - } - MiraiFileImpl(p) - } - - override val name: String - get() = if (absolutePath.matches(ROOT_REGEX)) absolutePath - else absolutePath.substringAfterLast('/') - - init { - checkName(absolutePath.substringAfterLast('/')) // do not check drive letter - } - - private fun checkName(name: String) { - name.substringAfterLast('/').forEach { c -> - if (c in """\/:?*"><|""") { - throw IllegalArgumentException("'${name}' contains illegal character '$c'.") - } - } - - memScoped { - val b = alloc() - CheckNameLegalDOS8Dot3A(absolutePath, nullPtr(), 0, nullPtr(), b.ptr) - if (b.value != 1) { - throw IllegalArgumentException("'${name}' contains illegal character.") - } - } - } - - override val length: Long - get() = useStat { it.st_size.convert() } ?: 0 - - - override val isFile: Boolean - get() = getFileAttributes() flag FILE_ATTRIBUTE_NORMAL - - override val isDirectory: Boolean - get() = getFileAttributes() flag FILE_ATTRIBUTE_DIRECTORY - - override fun exists(): Boolean = getFileAttributes() != INVALID_FILE_ATTRIBUTES - - private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesA(absolutePath) } - - override fun resolve(path: String): MiraiFile { - when (path) { - "." -> return this - ".." -> return parent ?: this // root - } - - if (ROOT_REGEX.find(path) != null) { // absolute - return MiraiFileImpl(path) - } - - return MiraiFileImpl(this.absolutePath + SEPARATOR + path) // assuming path is 'appendable' - } - - override fun resolve(file: MiraiFile): MiraiFile { - val parent = file.parent ?: return resolve(file.name) - return resolve(parent).resolve(file.name) - } - - override fun createNewFile(): Boolean { - memScoped { - // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea - val handle = CreateFileA( - absolutePath, - GENERIC_READ, - FILE_SHARE_WRITE, - nullPtr(), - CREATE_NEW, - FILE_ATTRIBUTE_NORMAL, - nullPtr() - ) - if (handle == NULL) return false - CloseHandle(handle) - return true - } - } - - override fun delete(): Boolean { - return if (isFile) { - DeleteFileA(absolutePath) == 0 - } else { - RemoveDirectoryA(absolutePath) == 0 - } - } - - override fun mkdir(): Boolean { - memScoped { - val v = alloc<_SECURITY_ATTRIBUTES>() - return CreateDirectoryA(absolutePath, v.ptr) == 0 - } - } - - override fun mkdirs(): Boolean { - if (this.parent?.mkdirs() == false) { - return false - } - return mkdir() - } - - override fun input(): Input { - val handle = fopen(absolutePath, "r") - if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'") - return PosixInputForFile(handle!!) - } - - override fun output(): Output { - val handle = fopen(absolutePath, "w") - if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'") - return PosixFileInstanceOutput(handle!!) - } -} \ No newline at end of file diff --git a/mirai-core-utils/src/mingwTest/kotlin/MiraiFileImplTest.kt b/mirai-core-utils/src/mingwTest/kotlin/MiraiFileImplTest.kt deleted file mode 100644 index f8bfbee5d..000000000 --- a/mirai-core-utils/src/mingwTest/kotlin/MiraiFileImplTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -package net.mamoe.mirai.utils - -import kotlin.math.absoluteValue -import kotlin.random.Random -import kotlin.test.Test -import kotlin.test.assertEquals - -internal class WindowsMiraiFileImplTest : AbstractNativeMiraiFileImplTest() { - private val rand = Random.nextInt().absoluteValue - override val baseTempDir: MiraiFile = MiraiFile.create("mirai_unit_tests") - override val tempPath = "mirai_unit_tests/temp$rand" - - @Test - override fun parent() { - assertEquals("C:/Users/Shared/mirai_test", tempDir.parent!!.absolutePath) - super.parent() - } - - @Test - override fun `resolve absolute`() { - MiraiFile.create("$tempPath/").resolve("C:/Users").let { - assertEquals("C:/Users", it.path) - assertEquals("C:/Users", it.absolutePath) - } - } -} \ No newline at end of file diff --git a/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt b/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt new file mode 100644 index 000000000..da57e46e3 --- /dev/null +++ b/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt @@ -0,0 +1,343 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.utils + +import io.ktor.utils.io.bits.* +import io.ktor.utils.io.core.* +import io.ktor.utils.io.errors.* +import kotlinx.cinterop.* +import platform.posix.* +import platform.windows.* + +private fun getFullPathName(path: String): String = memScoped { + ShortArray(MAX_PATH).usePinned { pin -> + val len = GetFullPathNameW(path, MAX_PATH, pin.addressOf(0).reinterpret(), null).toInt() + if (len != 0) { + return pin.get().toKStringFromUtf16(len) + } else { + when (val errno = errno) { + ENOTDIR -> return@memScoped path + EACCES -> return@memScoped path // permission denied + ENOENT -> return@memScoped path // no such file + else -> throw IllegalArgumentException( + "Invalid path($errno): $path", + cause = PosixException.forErrno(posixFunctionName = "GetFullPathNameW()") + ) + } + } + } +} + +private fun ShortArray.toKStringFromUtf16(len: Int): String { + val chars = CharArray(len) + var index = 0 + while (index < len) { + chars[index] = this[index].toInt().toChar() + ++index + } + return chars.concatToString() +} + +internal actual class MiraiFileImpl actual constructor( + // canonical + path: String +) : MiraiFile { + override val path = path.replace("/", "\\") + + actual companion object { + private val ROOT_REGEX = Regex("""^([a-zA-z]+:[/\\])""") + private const val SEPARATOR = '\\' + + @Suppress("UnnecessaryOptInAnnotation") + @OptIn(UnsafeNumber::class) + actual fun getWorkingDir(): MiraiFile { + val path = memScoped { + ByteArray(PATH_MAX).usePinned { + getcwd(it.addressOf(0), it.get().size.convert()) + it.get().toKString() + } + } + return MiraiFile.create(path) + } + } + + override val absolutePath: String by lazy { + val result = ROOT_REGEX.matchEntire(this.path) + ?: return@lazy getFullPathName(this.path).removeSuffix(SEPARATOR.toString()) + return@lazy result.groups.first()!!.value + } + + private fun Char.isSeparator() = this == '/' || this == '\\' + + override val parent: MiraiFile? by lazy { + if (ROOT_REGEX.matchEntire(this.path) != null) return@lazy null + val absolute = absolutePath + val p = absolute.substringBeforeLast(SEPARATOR, "") + if (p.isEmpty()) { + return@lazy null + } + if (p.lastOrNull() == ':') { + if (absolute.lastIndexOf(SEPARATOR) == p.lastIndex) { + // file is C:/ + return@lazy null + } else { + return@lazy MiraiFileImpl("$p/") // file is C:/xxx + } + } + MiraiFileImpl(p) + } + + override val name: String + get() = if (absolutePath.matches(ROOT_REGEX)) absolutePath + else absolutePath.substringAfterLast(SEPARATOR) + + init { + checkName(absolutePath.substringAfterLast(SEPARATOR)) // do not check drive letter + } + + private fun checkName(name: String) { + name.substringAfterLast(SEPARATOR).forEach { c -> + if (c in """\/:?*"><|""") { + throw IllegalArgumentException("'${name}' contains illegal character '$c'.") + } + } + +// memScoped { +// val b = alloc() +// CheckNameLegalDOS8Dot3A(absolutePath, nullPtr(), 0, nullPtr(), b.ptr) +// if (b.value != 1) { +// throw IllegalArgumentException("'${name}' contains illegal character.") +// } +// } + } + + override val length: Long + get() = useStat { it.st_size.convert() } ?: 0 +// memScoped { +// val handle = CreateFileW( +// absolutePath, +// GENERIC_READ, +// FILE_SHARE_READ, +// null, +// OPEN_EXISTING, +// FILE_ATTRIBUTE_NORMAL, +// null +// ) ?: return@memScoped 0 +// val length = alloc() +// if (GetFileSize(handle, length.ptr) == INVALID_FILE_SIZE) { +// if (GetLastError() == NO_ERROR.toUInt()) { +// return INVALID_FILE_SIZE.convert() +// } +// throw PosixException.forErrno(posixFunctionName = "GetFileSize()").wrapIO() +// } +// if (CloseHandle(handle) == FALSE) { +// throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() +// } +// length.value.convert() +// } + + + override val isFile: Boolean + get() = useStat { it.st_mode.convert() flag S_IFREG } ?: false + + override val isDirectory: Boolean + get() = useStat { it.st_mode.convert() flag S_IFDIR } ?: false + + override fun exists(): Boolean = getFileAttributes() != INVALID_FILE_ATTRIBUTES + + private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesW(absolutePath) } + + override fun resolve(path: String): MiraiFile { + when (path) { + "." -> return this + ".." -> return parent ?: this // root + } + + if (ROOT_REGEX.find(path) != null) { // absolute + return MiraiFileImpl(path) + } + + return MiraiFileImpl(this.absolutePath + SEPARATOR + path) // assuming path is 'appendable' + } + + override fun resolve(file: MiraiFile): MiraiFile { + val parent = file.parent ?: return resolve(file.name) + return resolve(parent).resolve(file.name) + } + + override fun createNewFile(): Boolean { + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea + val handle = CreateFileW( + absolutePath, + GENERIC_READ, + FILE_SHARE_DELETE, + null, + CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, + null + ) + if (handle == null || handle == INVALID_HANDLE_VALUE) { + return false + } + if (CloseHandle(handle) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() + } + return true + } + + override fun delete(): Boolean { + return if (isFile) { + DeleteFileW(absolutePath) != 0 + } else { + RemoveDirectoryW(absolutePath) != 0 + } + } + + override fun mkdir(): Boolean { + memScoped { + val v = alloc<_SECURITY_ATTRIBUTES>() + return CreateDirectoryW(absolutePath, v.ptr) != 0 + } + } + + override fun mkdirs(): Boolean { + this.parent?.mkdirs() + return mkdir() + } + + override fun input(): Input { +// println(absolutePath) +// val handle2 = fopen(absolutePath, "rb") ?:throw IOException( +// "Failed to open file '$absolutePath'", +// PosixException.forErrno(posixFunctionName = "fopen()") +// ) +// return PosixInputForFile(handle2) + // Will get I/O operation failed due to posix error code 2 + + val handle = CreateFileW( + absolutePath, + GENERIC_READ, + FILE_SHARE_DELETE, + null, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + null + ) + if (handle == null || handle == INVALID_HANDLE_VALUE) throw IOException( + "Failed to open file '$absolutePath'", + PosixException.forErrno(posixFunctionName = "CreateFileW()") + ) + return WindowsFileInput(handle) + } + + override fun output(): Output { +// val handle2 = fopen(absolutePath, "wb") +// ?: throw IOException( +// "Failed to open file '$absolutePath'", +// PosixException.forErrno(posixFunctionName = "fopen()") +// ) +// return PosixFileInstanceOutput(handle) +// + val handle = CreateFileW( + absolutePath, + GENERIC_WRITE, + FILE_SHARE_DELETE, + null, + (if (exists()) TRUNCATE_EXISTING else CREATE_NEW).toUInt(), + FILE_ATTRIBUTE_NORMAL, + null + ) + if (handle == null || handle == INVALID_HANDLE_VALUE) throw IOException( + "Failed to open file '$absolutePath'", + PosixException.forErrno(posixFunctionName = "CreateFileW()") + ) + return WindowsFileOutput(handle) + } + + override fun hashCode(): Int { + return this.path.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other == null) return false + if (!isSameType(this, other)) return false + return this.path == other.path + } + + override fun toString(): String { + return "MiraiFileImpl($path)" + } +} + + +internal class WindowsFileInput(private val file: HANDLE) : Input() { + private var closed = false + + override fun fill(destination: Memory, offset: Int, length: Int): Int { + if (file == INVALID_HANDLE_VALUE) return 0 + + memScoped { + val n = alloc() + if (ReadFile(file, destination.pointer + offset, length.convert(), n.ptr, null) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "ReadFile()").wrapIO() + } + + return n.value.convert().toInt() + } + } + + override fun closeSource() { + if (closed) return + closed = true + + if (file != INVALID_HANDLE_VALUE) { + if (CloseHandle(file) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() + } + } + } +} + +@Suppress("DEPRECATION") +internal class WindowsFileOutput(private val file: HANDLE) : Output() { + private var closed = false + + override fun flush(source: Memory, offset: Int, length: Int) { + val end = offset + length + var currentOffset = offset + + memScoped { + val written = alloc() + while (currentOffset < end) { + val result = WriteFile( + file, + source.pointer + currentOffset.convert(), + (end - currentOffset).convert(), + written.ptr, + null + ).convert() + if (result == FALSE) { + throw PosixException.forErrno(posixFunctionName = "WriteFile()").wrapIO() + } + currentOffset += written.value.toInt() + } + + } + } + + override fun closeDestination() { + if (closed) return + closed = true + + if (CloseHandle(file) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() + } + } +} diff --git a/mirai-core-utils/src/mingwMain/kotlin/StandardUtils.kt b/mirai-core-utils/src/mingwX64Main/kotlin/StandardUtils.kt similarity index 74% rename from mirai-core-utils/src/mingwMain/kotlin/StandardUtils.kt rename to mirai-core-utils/src/mingwX64Main/kotlin/StandardUtils.kt index 0d77f28fd..d7c86e5fc 100644 --- a/mirai-core-utils/src/mingwMain/kotlin/StandardUtils.kt +++ b/mirai-core-utils/src/mingwX64Main/kotlin/StandardUtils.kt @@ -11,4 +11,6 @@ package net.mamoe.mirai.utils import platform.windows.GetCurrentProcessorNumber -public actual fun availableProcessors(): Int = GetCurrentProcessorNumber().toInt() \ No newline at end of file + +public actual fun availableProcessors(): Int = + GetCurrentProcessorNumber().toInt().coerceAtLeast(4) // somehow it worked on my machine but not on CI \ No newline at end of file diff --git a/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt b/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt new file mode 100644 index 000000000..9562dee6a --- /dev/null +++ b/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.utils + +import kotlin.math.absoluteValue +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class WindowsMiraiFileImplTest : AbstractNativeMiraiFileImplTest() { + private val rand = Random.nextInt().absoluteValue + override val baseTempDir: MiraiFile = MiraiFile.create("C:\\mirai_unit_tests") + override val tempPath = "C:\\mirai_unit_tests\\temp$rand" + + @Test + override fun parent() { + assertEquals("C:\\mirai_unit_tests", tempDir.parent!!.absolutePath) + super.parent() + } + + override fun `canonical paths for non-canonical input`() { + super.`canonical paths for non-canonical input`() + + // extra /sss/.. + MiraiFile.create("$tempPath/sss/..").resolve("test").let { + assertPathEquals("${tempPath}/test", it.path) // Windows resolves always + assertPathEquals("${tempPath}/test", it.absolutePath) + } + } + + @Test + override fun `resolve absolute`() { + MiraiFile.create("$tempPath/").resolve("C:\\mirai_unit_tests").let { + assertEquals("C:\\mirai_unit_tests", it.path) + assertEquals("C:\\mirai_unit_tests", it.absolutePath) + } + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/Clock.kt b/mirai-core-utils/src/nativeMain/kotlin/Clock.kt index 42def5877..5a15e11a4 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/Clock.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/Clock.kt @@ -12,5 +12,7 @@ package net.mamoe.mirai.utils public actual inline fun measureTimeMillis(block: () -> Unit): Long { - return kotlin.system.measureTimeMillis(block) + val start = currentTimeMillis() // getTimeMillis in stdlib doesn't work on some native targets. + block() + return currentTimeMillis() - start } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/ExceptionCollector.kt b/mirai-core-utils/src/nativeMain/kotlin/ExceptionCollector.kt index 076f1f9c7..38386cdaa 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/ExceptionCollector.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/ExceptionCollector.kt @@ -10,10 +10,13 @@ package net.mamoe.mirai.utils internal actual fun hash(e: Throwable): Long { - // Stacktrace analysis not available var hashCode = 1L - for (stackTraceAddress in e.getStackTraceAddresses()) { + val trace = e.getStackTraceAddresses() + for (stackTraceAddress in trace) { hashCode = (hashCode xor stackTraceAddress).shl(1) } - return hashCode + + // Somehow stacktrace analysis is on my own Windows machine but not on GitHub Actions. + // Hashing with a class to tentatively not filter out different types. + return hashCode xor e::class.hashCode().toLongUnsigned() } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/MiraiFile.kt b/mirai-core-utils/src/nativeMain/kotlin/MiraiFile.kt index f652d61cd..308bd3b86 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/MiraiFile.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/MiraiFile.kt @@ -215,5 +215,5 @@ internal class PosixInputForFile(val file: CPointer) : AbstractInput() { } @OptIn(ExperimentalIoApi::class) -internal fun PosixException.wrapIO(): IOException = +public fun PosixException.wrapIO(): IOException = IOException("I/O operation failed due to posix error code $errno", this) diff --git a/mirai-core-utils/src/nativeMain/kotlin/Service.kt b/mirai-core-utils/src/nativeMain/kotlin/Service.kt index 9530e4144..4bbeb98e8 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/Service.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/Service.kt @@ -38,12 +38,18 @@ public object Services { } } - public fun implementations(baseClass: String): List? { + public fun implementations(baseClass: String): List>? { lock.withLock { return registered[baseClass]?.map { it.instance } } } + + public fun print(): String { + lock.withLock { + return registered.entries.joinToString { "${it.key}:${it.value}" } + } + } } @Suppress("UNCHECKED_CAST") @@ -57,10 +63,10 @@ public actual fun loadService( clazz: KClass, fallbackImplementation: String? ): T = loadServiceOrNull(clazz, fallbackImplementation) - ?: error("Could not load service '${clazz.qualifiedName ?: clazz}'") + ?: error("Could not load service '${clazz.qualifiedName ?: clazz}'. Current services: ${Services.print()}") public actual fun loadServices(clazz: KClass): Sequence = - Services.implementations(qualifiedNameOrFail(clazz))?.asSequence().orEmpty().castUp() + Services.implementations(qualifiedNameOrFail(clazz))?.asSequence()?.map { it.value }.orEmpty().castUp() private fun qualifiedNameOrFail(clazz: KClass) = clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz") \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt b/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt index f9c06e238..7d91c836d 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/TimeUtils.kt @@ -11,6 +11,8 @@ package net.mamoe.mirai.utils +import kotlinx.atomicfu.locks.ReentrantLock +import kotlinx.atomicfu.locks.withLock import kotlinx.cinterop.* import platform.posix.* @@ -20,23 +22,28 @@ import platform.posix.* public actual fun currentTimeMillis(): Long { // Do not use getTimeMillis from stdlib, it doesn't support iosSimulatorArm64 memScoped { - val timeT = alloc() - time(timeT.ptr) - return timeT.value.toLongUnsigned() + val spec = alloc() + clock_gettime(CLOCK_REALTIME.convert(), spec.ptr) + return (spec.tv_sec * 1000 + spec.tv_nsec / 1e6).toLong() } } -public actual fun currentTimeFormatted(format: String?): String { + +private val timeLock = ReentrantLock() + +@OptIn(UnsafeNumber::class) +public actual fun currentTimeFormatted(format: String?): String = timeLock.withLock { memScoped { val timeT = alloc() time(timeT.ptr) - val tm = localtime(timeT.ptr) - try { - val bb = allocArray(40) - strftime(bb, 40, "%Y-%M-%d %H:%M:%S", tm); - return bb.toKString() - } finally { - free(tm) - } + + // http://www.cplusplus.com/reference/clibrary/ctime/localtime/ + // tm returns a static pointer which doesn't need to free + val tm = localtime(timeT.ptr) // localtime is not thread-safe + + val bb = allocArray(40) + strftime(bb, 40, "%Y-%m-%d %H:%M:%S", tm); + + bb.toKString() } } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/getProperty.kt b/mirai-core-utils/src/nativeMain/kotlin/getProperty.kt index 43a7583b1..1ee367d81 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/getProperty.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/getProperty.kt @@ -9,10 +9,12 @@ package net.mamoe.mirai.utils +private val properties: MutableMap = ConcurrentHashMap() + internal actual fun getProperty(name: String, default: String): String? { - TODO("Not yet implemented") + return properties.getOrElse(name) { default } } internal actual fun setProperty(name: String, value: String) { - TODO("Not yet implemented") + properties[name] = value } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt b/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt index 88515a7e2..2c94a6236 100644 --- a/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt +++ b/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt @@ -24,7 +24,7 @@ internal abstract class AbstractNativeMiraiFileImplTest { @AfterTest fun afterTest() { println("Cleaning up...") - baseTempDir.deleteRecursively() + println("deleteRecursively:" + baseTempDir.deleteRecursively()) } @BeforeTest @@ -35,8 +35,8 @@ internal abstract class AbstractNativeMiraiFileImplTest { @Test fun `canonical paths for canonical input`() { - assertEquals(tempPath, tempDir.path) - assertEquals(tempPath, tempDir.absolutePath) + assertPathEquals(tempPath, tempDir.path) + assertPathEquals(tempPath, tempDir.absolutePath) } @Test @@ -46,31 +46,26 @@ internal abstract class AbstractNativeMiraiFileImplTest { } @Test - fun `canonical paths for non-canonical input`() { + open fun `canonical paths for non-canonical input`() { // extra / MiraiFile.create("$tempPath/").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } // extra // MiraiFile.create("$tempPath//").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } // extra /. MiraiFile.create("$tempPath/.").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } // extra /./. MiraiFile.create("$tempPath/./.").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) - } - // extra /sss/.. - MiraiFile.create("$tempPath/sss/..").resolve("test").let { - assertEquals("${tempPath}/sss/../test", it.path) // because file is not found - assertEquals("${tempPath}/sss/../test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } } @@ -90,7 +85,7 @@ internal abstract class AbstractNativeMiraiFileImplTest { assertFalse { tempDir.resolve("not_existing_dir").exists() } assertEquals(0L, tempDir.resolve("not_existing_dir").length) assertTrue { tempDir.resolve("not_existing_dir").mkdir() } - assertNotEquals(0L, tempDir.resolve("not_existing_dir").length) +// assertNotEquals(0L, tempDir.resolve("not_existing_dir").length) // length is platform-dependent, on Windows it is 0 but on unix it is not assertTrue { tempDir.resolve("not_existing_dir").exists() } } @@ -98,20 +93,27 @@ internal abstract class AbstractNativeMiraiFileImplTest { fun `isFile isDirectory`() { assertTrue { tempDir.exists() } + println("1") assertFalse { tempDir.resolve("not_existing_file.txt").exists() } assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile) + println("1") assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() } assertEquals(true, tempDir.resolve("not_existing_file.txt").isFile) assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_file.txt").exists() } + println("1") assertFalse { tempDir.resolve("not_existing_dir").exists() } assertEquals(false, tempDir.resolve("not_existing_dir").isFile) assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_dir").mkdir() } assertEquals(false, tempDir.resolve("not_existing_dir").isFile) assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_dir").exists() } } @@ -134,7 +136,7 @@ internal abstract class AbstractNativeMiraiFileImplTest { @Test fun readText() { - tempDir.resolve("readText1.txt").let { file -> + tempDir.resolve("readText2.txt").let { file -> assertTrue { !file.exists() } assertFailsWith { file.readText() } @@ -143,4 +145,39 @@ internal abstract class AbstractNativeMiraiFileImplTest { assertEquals(text, file.readText()) } } + + private val bigText = "some text".repeat(10000) + + @Test + fun writeBigText() { + // new file + tempDir.resolve("writeText3.txt").let { file -> + file.writeText(bigText) + assertEquals(bigText.length, file.length.toInt()) + } + + // override + tempDir.resolve("writeText4.txt").let { file -> + file.writeText(bigText) + assertEquals(bigText.length, file.length.toInt()) + } + } + + @Test + fun readBigText() { + tempDir.resolve("readText4.txt").let { file -> + assertTrue { !file.exists() } + assertFailsWith { file.readText() } + + file.writeText(bigText) + println("reading text") + val read = file.readText() + assertEquals(bigText.length, read.length) + assertEquals(bigText, read) + } + } + + protected fun assertPathEquals(expected: String, actual: String, message: String? = null) { + asserter.assertEquals(message, expected.replace("\\", "/"), actual.replace("\\", "/")) + } } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt b/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt index df528605f..951a5e3df 100644 --- a/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt +++ b/mirai-core-utils/src/nativeTest/kotlin/TimeUtilsTest.kt @@ -17,7 +17,7 @@ internal class TimeUtilsTest { @Test fun `can get currentTimeMillis`() { val time = currentTimeMillis() - assertTrue(time.toString()) { time > 1642549113 } + assertTrue(time.toString()) { time > 1654209523269 } } @Test diff --git a/mirai-core-utils/src/unixMain/kotlin/MiraiFileImpl.kt b/mirai-core-utils/src/unixMain/kotlin/MiraiFileImpl.kt index 4e7c0e659..9cfe54a8c 100644 --- a/mirai-core-utils/src/unixMain/kotlin/MiraiFileImpl.kt +++ b/mirai-core-utils/src/unixMain/kotlin/MiraiFileImpl.kt @@ -157,7 +157,7 @@ internal actual class MiraiFileImpl actual constructor( @OptIn(ExperimentalIoApi::class) override fun input(): Input { - val handle = fopen(absolutePath, "r") + val handle = fopen(absolutePath, "rb") ?: throw IOException( "Failed to open file '$absolutePath'", PosixException.forErrno(posixFunctionName = "fopen()") @@ -167,7 +167,7 @@ internal actual class MiraiFileImpl actual constructor( @OptIn(ExperimentalIoApi::class) override fun output(): Output { - val handle = fopen(absolutePath, "w") + val handle = fopen(absolutePath, "wb") ?: throw IOException( "Failed to open file '$absolutePath'", PosixException.forErrno(posixFunctionName = "fopen()") diff --git a/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt b/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt index 91d7751c8..05d17c5e0 100644 --- a/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt +++ b/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt @@ -28,6 +28,16 @@ internal class UnixMiraiFileImplTest : AbstractNativeMiraiFileImplTest() { super.parent() } + override fun `canonical paths for non-canonical input`() { + super.`canonical paths for non-canonical input`() + + // extra /sss/.. + MiraiFile.create("$tempPath/sss/..").resolve("test").let { + assertPathEquals("${tempPath}/sss/../test", it.path) // because file is not found + assertPathEquals("${tempPath}/sss/../test", it.absolutePath) + } + } + @Test override fun `resolve absolute`() { MiraiFile.create("$tempPath/").resolve("/Users").let { diff --git a/mirai-core/.gitignore b/mirai-core/.gitignore index 53088f627..a61e06ca2 100644 --- a/mirai-core/.gitignore +++ b/mirai-core/.gitignore @@ -1 +1,2 @@ -src/jvmTest/kotlin/local \ No newline at end of file +src/jvmTest/kotlin/local +test-sandbox/ \ No newline at end of file diff --git a/mirai-core/build.gradle.kts b/mirai-core/build.gradle.kts index b08e71316..04ef01019 100644 --- a/mirai-core/build.gradle.kts +++ b/mirai-core/build.gradle.kts @@ -10,6 +10,7 @@ @file:Suppress("UNUSED_VARIABLE") import BinaryCompatibilityConfigurator.configureBinaryValidators +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { kotlin("multiplatform") @@ -57,6 +58,8 @@ kotlin { implementation(bouncycastle) implementation(`log4j-api`) implementation(`netty-all`) + implementation(`ktor-client-okhttp`) + api(`kotlinx-coroutines-core`) } } @@ -102,6 +105,55 @@ kotlin { dependencies { } } + + NATIVE_TARGETS.forEach { target -> + (targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("OpenSSL") + .apply { + defFile = projectDir.resolve("src/nativeMain/cinterop/OpenSSL.def") + packageName("openssl") + } + } + + UNIX_LIKE_TARGETS.forEach { target -> + (targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("Socket") + .apply { + defFile = projectDir.resolve("src/unixMain/cinterop/Socket.def") + packageName("sockets") + } + } + + WIN_TARGETS.forEach { target -> + (targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("Socket") + .apply { + defFile = projectDir.resolve("src/mingwX64Main/cinterop/Socket.def") + packageName("sockets") + } + } + + configure(WIN_TARGETS.map { getByName(it + "Main") }) { + dependencies { + implementation(`ktor-client-curl`) + } + } + + configure(LINUX_TARGETS.map { getByName(it + "Main") }) { + dependencies { + implementation(`ktor-client-curl`) + } + } + + val darwinMain by getting { + dependencies { + implementation(`ktor-client-ios`) + } + } + + disableCrossCompile() +// val unixMain by getting { +// dependencies { +// implementation(`ktor-client-cio`) +// } +// } } } diff --git a/mirai-core/src/androidTest/kotlin/android/util/Log.kt b/mirai-core/src/androidTest/kotlin/android/util/Log.kt new file mode 100644 index 000000000..f56064097 --- /dev/null +++ b/mirai-core/src/androidTest/kotlin/android/util/Log.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package android.util + +import net.mamoe.mirai.internal.utils.StdoutLogger + +// Dummy implementation for tests, since we don't have an Android SDK + +@Suppress("UNUSED_PARAMETER", "unused") +object Log { + const val VERBOSE = 2 + const val DEBUG = 3 + const val INFO = 4 + const val WARN = 5 + const val ERROR = 6 + const val ASSERT = 7 + + private val stdout = StdoutLogger("AndroidLog") + + @JvmStatic + fun v(tag: String?, msg: String?): Int { + stdout.verbose(msg) + return 0 + } + + @JvmStatic + fun v(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.verbose(msg, tr) + return 0 + } + + @JvmStatic + fun d(tag: String?, msg: String?): Int { + stdout.debug(msg, tr) + return 0 + } + + @JvmStatic + fun d(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.debug(msg, tr) + return 0 + } + + @JvmStatic + fun i(tag: String?, msg: String?): Int { + stdout.info(msg, tr) + return 0 + } + + @JvmStatic + fun i(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.info(msg, tr) + return 0 + } + + @JvmStatic + fun w(tag: String?, msg: String?): Int { + stdout.warning(msg, tr) + return 0 + } + + @JvmStatic + fun w(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.warning(msg, tr) + return 0 + } + + @JvmStatic + fun w(tag: String?, tr: Throwable?): Int { + stdout.warning(msg, tr) + return 0 + } + + @JvmStatic + fun e(tag: String?, msg: String?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun e(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun wtf(tag: String?, msg: String?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun wtf(tag: String?, tr: Throwable?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun wtf(tag: String?, msg: String?, tr: Throwable?): Int { + stdout.error(msg, tr) + return 0 + } + + @JvmStatic + fun getStackTraceString(tr: Throwable): String { + return tr.stackTraceToString() + } + + @JvmStatic + fun println(priority: Int, tag: String?, msg: String?): Int { + stdout.info(msg, tr) + return 0 + } + + + private inline val tr get() = null + private inline val msg get() = null +} diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 5b68ae763..c4c719265 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -7,9 +7,12 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:JvmName("MiraiImplKt_common") + package net.mamoe.mirai.internal import io.ktor.client.* +import io.ktor.client.engine.* import io.ktor.client.features.* import io.ktor.client.request.* import io.ktor.client.request.forms.* @@ -31,25 +34,14 @@ import net.mamoe.mirai.internal.contact.info.MemberInfoImpl import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl.Companion.impl import net.mamoe.mirai.internal.event.EventChannelToEventDispatcherAdapter import net.mamoe.mirai.internal.event.InternalEventMechanism -import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep +import net.mamoe.mirai.internal.message.EmptyRefineContext +import net.mamoe.mirai.internal.message.RefineContext +import net.mamoe.mirai.internal.message.SimpleRefineContext import net.mamoe.mirai.internal.message.data.* -import net.mamoe.mirai.internal.message.data.FileMessageImpl -import net.mamoe.mirai.internal.message.data.OfflineAudioImpl -import net.mamoe.mirai.internal.message.data.OnlineAudioImpl -import net.mamoe.mirai.internal.message.data.UnsupportedMessageImpl import net.mamoe.mirai.internal.message.image.* -import net.mamoe.mirai.internal.message.image.OfflineGroupImage -import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl -import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl import net.mamoe.mirai.internal.message.source.* -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromFriendImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromGroupImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromStrangerImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromTempImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToFriendImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToStrangerImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToTempImpl +import net.mamoe.mirai.internal.message.toMessageChainNoSource import net.mamoe.mirai.internal.network.components.EventDispatcher import net.mamoe.mirai.internal.network.highway.ChannelKind import net.mamoe.mirai.internal.network.highway.ResourceKind @@ -77,14 +69,21 @@ import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.action.Nudge import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* +import kotlin.jvm.JvmName internal fun getMiraiImpl() = Mirai as MiraiImpl +internal expect fun createDefaultHttpClient(): HttpClient + +@Suppress("FunctionName") +internal expect fun _MiraiImpl_static_init() + @OptIn(LowLevelApi::class) // not object for ServiceLoader. internal open class MiraiImpl : IMirai, LowLevelApiAccessor { companion object { init { + _MiraiImpl_static_init() MessageSerializers.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer()) MessageSerializers.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer()) MessageSerializers.registerSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.serializer()) @@ -156,13 +155,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault @Deprecated("Mirai is not going to use ktor. This is deprecated for removal.", level = DeprecationLevel.WARNING) - override var Http: HttpClient = HttpClient() { - install(HttpTimeout) { - this.requestTimeoutMillis = 30_0000 - this.connectTimeoutMillis = 30_0000 - this.socketTimeoutMillis = 30_0000 - } - } + override var Http: HttpClient = createDefaultHttpClient() override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) { @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @@ -870,17 +863,21 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { return main.toForwardMessageNodes(bot, context) } - protected open suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node { + private suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node { val msg = this + + @Suppress("USELESS_CAST") // compiler bug, do not remove + val senderName = (msg.msgHead.groupInfo?.groupCard + ?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() } + ?: msg.msgHead.fromUin.toString()) as String + val chain = listOf(msg) + .toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP) + .refineDeep(bot, refineContext) return ForwardMessage.Node( senderId = msg.msgHead.fromUin, time = msg.msgHead.msgTime, - senderName = msg.msgHead.groupInfo?.groupCard - ?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() } - ?: msg.msgHead.fromUin.toString(), - messageChain = listOf(msg) - .toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP) - .refineDeep(bot, refineContext) + senderName = senderName, + messageChain = chain ) } diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 44321f4ad..3d63afd12 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -52,7 +52,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache import net.mamoe.mirai.internal.utils.ImagePatcher -import net.mamoe.mirai.internal.utils.RemoteFileImpl import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.subLogger import net.mamoe.mirai.message.MessageReceipt @@ -117,8 +116,18 @@ private val logger by lazy { internal fun Bot.nickIn(context: Contact): String = if (context is Group) context.botAsMember.nameCardOrNick else bot.nick +internal expect class GroupImpl constructor( + bot: QQAndroidBot, + parentCoroutineContext: CoroutineContext, + id: Long, + groupInfo: GroupInfo, + members: ContactList, +) : Group, CommonGroupImpl { + companion object +} + @Suppress("PropertyName") -internal class GroupImpl constructor( +internal abstract class CommonGroupImpl constructor( bot: QQAndroidBot, parentCoroutineContext: CoroutineContext, override val id: Long, @@ -128,31 +137,27 @@ internal class GroupImpl constructor( companion object val uin: Long = groupInfo.uin - override val settings: GroupSettingsImpl = GroupSettingsImpl(this, groupInfo) - override var name: String by settings::name + final override val settings: GroupSettingsImpl = GroupSettingsImpl(this.cast(), groupInfo) + final override var name: String by settings::name - override lateinit var owner: NormalMemberImpl - override lateinit var botAsMember: NormalMemberImpl + final override lateinit var owner: NormalMemberImpl + final override lateinit var botAsMember: NormalMemberImpl internal val botAsMemberInitialized get() = ::botAsMember.isInitialized - @Suppress("DEPRECATION") - @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.WARNING) - @DeprecatedSinceMirai(warningSince = "2.8") - override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") } - override val files: RemoteFiles by lazy { RemoteFilesImpl(this) } + final override val files: RemoteFiles by lazy { RemoteFilesImpl(this) } val lastTalkative = atomic(null) - override val announcements: Announcements by lazy { + final override val announcements: Announcements by lazy { AnnouncementsImpl( - this, + this as GroupImpl, bot.network.logger.subLogger("Group $id") ) } val groupPkgMsgParsingCache = GroupPkgMsgParsingCache() - private val messageProtocolStrategy: MessageProtocolStrategy = GroupMessageProtocolStrategy(this) + private val messageProtocolStrategy: MessageProtocolStrategy = GroupMessageProtocolStrategy(this.cast()) override suspend fun quit(): Boolean { check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" } @@ -162,11 +167,11 @@ internal class GroupImpl constructor( } val response: ProfileService.GroupMngReq.GroupMngReqResponse = bot.network.sendAndExpect( - ProfileService.GroupMngReq(bot.client, this@GroupImpl.id), 5000, 2 + ProfileService.GroupMngReq(bot.client, this@CommonGroupImpl.id), 5000, 2 ) check(response.errorCode == 0) { "Group.quit failed: $response".also { - bot.groups.delegate.add(this@GroupImpl) + bot.groups.delegate.add(this@CommonGroupImpl.castUp()) } } BotLeaveEvent.Active(this).broadcast() @@ -186,7 +191,7 @@ internal class GroupImpl constructor( check(!isBotMuted) { throw BotIsBeingMutedException(this, message) } return sendMessageImpl( message, - messageProtocolStrategy, + messageProtocolStrategy.castUp(), ::GroupMessagePreSendEvent, ::GroupMessagePostSendEvent.cast() ) @@ -220,7 +225,8 @@ internal class GroupImpl constructor( when (response) { is ImgStore.GroupPicUp.Response.Failed -> { - ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast() + ImageUploadEvent.Failed(this@CommonGroupImpl, resource, response.resultCode, response.message) + .broadcast() if (response.message == "over file size max") throw OverFileSizeMaxException() error("upload group image failed with reason ${response.message}") } @@ -239,7 +245,7 @@ internal class GroupImpl constructor( it.fileId = response.fileId.toInt() } .also { it.putIntoCache() } - .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() } + .also { ImageUploadEvent.Succeed(this@CommonGroupImpl, resource, it).broadcast() } } is ImgStore.GroupPicUp.Response.RequireUpload -> { // val servers = response.uploadIpList.zip(response.uploadPortList) @@ -268,7 +274,7 @@ internal class GroupImpl constructor( ) }.also { it.fileId = response.fileId.toInt() } .also { it.putIntoCache() } - .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() } + .also { ImageUploadEvent.Succeed(this@CommonGroupImpl, resource, it).broadcast() } } } } @@ -350,7 +356,7 @@ internal class GroupImpl constructor( val result = bot.network.sendAndExpect( TroopEssenceMsgManager.SetEssence( bot.client, - this@GroupImpl.uin, + this@CommonGroupImpl.uin, source.internalIds.first(), source.ids.first() ), 5000, 2 diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt index bb79eaa70..cf3111f08 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt @@ -30,6 +30,7 @@ internal abstract class MessageProtocol( } fun collectProcessors(processorCollector: ProcessorCollector) { + println("collectProcessors, this=$this, class=${this::class}") processorCollector.collectProcessorsImpl() } diff --git a/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt b/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt index 065565618..3a19ddd66 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/EcdhInitialPublicKeyUpdater.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.internal.network.components import io.ktor.client.request.* +import kotlinx.coroutines.withTimeout import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -22,6 +23,7 @@ import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.currentTimeSeconds +import kotlin.time.Duration.Companion.seconds /** @@ -88,7 +90,7 @@ internal class EcdhInitialPublicKeyUpdaterImpl( logger.info("ECDH key is invalid, start to fetch ecdh public key from server.") val respStr = @Suppress("DEPRECATION", "DEPRECATION_ERROR") - Mirai.Http.get("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}") + withTimeout(10.seconds) { Mirai.Http.get("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}") } val resp = json.decodeFromString(ServerRespPOJO.serializer(), respStr) resp.pubKeyMeta.let { meta -> val isValid = ECDH.verifyPublicKey( diff --git a/mirai-core/src/commonMain/kotlin/network/components/ServerList.kt b/mirai-core/src/commonMain/kotlin/network/components/ServerList.kt index 0d41f8260..16c3adc1a 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/ServerList.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/ServerList.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.components.ServerList.Companion.DEFAULT_SERVER_LIST import net.mamoe.mirai.internal.network.handler.SocketAddress +import net.mamoe.mirai.internal.network.handler.createSocketAddress import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.TestOnly import net.mamoe.mirai.utils.info @@ -33,7 +34,7 @@ internal data class ServerAddress( return "$host:$port" } - fun toSocketAddress(): SocketAddress = SocketAddress(host, port) + fun toSocketAddress(): SocketAddress = createSocketAddress(host, port) } /** diff --git a/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt index 444a16d20..ed0a38d24 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt @@ -11,6 +11,8 @@ package net.mamoe.mirai.internal.network.handler import io.ktor.utils.io.core.* import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.onFailure import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector @@ -89,14 +91,67 @@ internal abstract class CommonNetworkHandler( internal inner class PacketDecodePipeline(parentContext: CoroutineContext) : CoroutineScope by parentContext.childScope() { private val packetCodec: PacketCodec by lazy { context[PacketCodec] } + private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] } - fun send(raw: RawIncomingPacket) { + + private val queue: Channel = Channel(Channel.BUFFERED) { undelivered -> + launch { sendQueue(undelivered) } + }.also { channel -> coroutineContext[Job]!!.invokeOnCompletion { channel.close(it) } } + + private suspend inline fun sendQueue(packet: ByteReadPacket) { + queue.send(packet) + } + + init { launch { - packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" } - val result = packetCodec.processBody(context.bot, raw) - if (result == null) { - collectUnknownPacket(raw) - } else collectReceived(result) + while (isActive) { + val result = queue.receiveCatching() + packetLogger.verbose { "Decoding packet: $result" } + result.onFailure { if (it is CancellationException) return@launch } + + result.getOrNull()?.let { packet -> + try { + val decoded = decodePacket(packet) + processBody(decoded) + } catch (e: Throwable) { + if (e is CancellationException) return@launch + handleExceptionInDecoding(e) + logger.error("Error while decoding packet '${packet}'", e) + } + } + } + } + } + + private fun decodePacket(packet: ByteReadPacket): RawIncomingPacket { + return if (packetLogger.isDebugEnabled) { + val bytes = packet.readBytes() + logger.verbose { "Decoding: len=${bytes.size}, value=${bytes.toUHexString()}" } + val raw = packetCodec.decodeRaw( + ssoProcessor.ssoSession, + bytes.toReadPacket() + ) + logger.verbose { "Decoded: ${raw.commandName}" } + raw + } else { + packetCodec.decodeRaw( + ssoProcessor.ssoSession, + packet + ) + } + } + + private suspend fun processBody(raw: RawIncomingPacket) { + packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" } + val result = packetCodec.processBody(context.bot, raw) + if (result == null) { + collectUnknownPacket(raw) + } else collectReceived(result) + } + + fun send(packet: ByteReadPacket) { + queue.trySend(packet).onFailure { + throw it ?: throw IllegalStateException("Internal error: Failed to decode '$packet' without reason.") } } } @@ -145,8 +200,6 @@ internal abstract class CommonNetworkHandler( this.setState { StateConnecting(ExceptionCollector()) } ?.resumeConnection() ?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread. - - println("INITIALIZED RETURN") } override fun toString(): String = "StateInitialized" @@ -214,9 +267,6 @@ internal abstract class CommonNetworkHandler( connectResult.await() // propagates exceptions val connection = connection.await() this.setState { StateLoading(connection) } - .also { - println(" this.setState { StateLoading(connection) }: " + it) - } ?.resumeConnection() ?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread. } diff --git a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt index 9fa66d317..6bc293a08 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt @@ -27,9 +27,12 @@ internal expect fun interface NetworkHandlerFactory { } } -internal expect abstract class SocketAddress { - val host: String - val port: Int -} +internal expect abstract class SocketAddress -internal expect fun SocketAddress(host: String, port: Int): SocketAddress +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +internal expect fun SocketAddress.getHost(): String + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +internal expect fun SocketAddress.getPort(): Int + +internal expect fun createSocketAddress(host: String, port: Int): SocketAddress diff --git a/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt b/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt index 35e537efd..6fbbd4ea0 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.utils.coroutineName import net.mamoe.mirai.utils.debug import net.mamoe.mirai.utils.systemProp import kotlin.coroutines.coroutineContext +import kotlin.native.concurrent.ThreadLocal internal class LoggingStateObserver( val logger: MiraiLogger, @@ -72,6 +73,7 @@ internal class LoggingStateObserver( ) } + @ThreadLocal companion object { /** * - `on`/`true` for simple logging diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt index c0f942f9c..83713d301 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt @@ -422,5 +422,5 @@ internal fun String.toIpV4Long(): Long { if (isEmpty()) return 0 val split = split('.') if (split.size != 4) return 0 - return split.mapToByteArray { it.toByte() }.toInt().toLongUnsigned() + return split.mapToByteArray { it.toUByte().toByte() }.toInt().toLongUnsigned() } diff --git a/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt b/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt index 7ba46c52f..c4a556cac 100644 --- a/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt +++ b/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.internal.pipeline -import io.ktor.util.collections.* import io.ktor.utils.io.core.* import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext diff --git a/mirai-core/src/commonMain/kotlin/utils/FileSystem.kt b/mirai-core/src/commonMain/kotlin/utils/FileSystem.kt new file mode 100644 index 000000000..0487c8086 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/FileSystem.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils + +// internal for tests +internal object FileSystem { + fun checkLegitimacy(path: String) { + val char = path.firstOrNull { it in """:*?"<>|""" } + if (char != null) { + throw IllegalArgumentException("""Chars ':*?"<>|' are not allowed in path. RemoteFile path contains illegal char: '$char'. path='$path'""") + } + } + + fun isLegal(path: String): Boolean { + return path.firstOrNull { it in """:*?"<>|""" } == null + } + + fun normalize(path: String): String { + checkLegitimacy(path) + return path.replace('\\', '/') + } + + // net.mamoe.mirai.internal.utils.internal.utils.FileSystemTest + + fun normalize(parent: String, name: String): String { + var nName = normalize(name) + if (nName.startsWith('/')) return nName // absolute path then ignore parent + nName = nName.removeSuffix("/") + + var nParent = normalize(parent) + if (nParent == "/") return "/$nName" + if (!nParent.startsWith('/')) nParent = "/$nParent" + + val slash = nName.indexOf('/') + if (slash != -1) { + nParent += '/' + nName.substring(0, slash) + nName = nName.substring(slash + 1) + } + + return "$nParent/$nName" + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt b/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt index 32e8f5882..985b47d57 100644 --- a/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt +++ b/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt @@ -7,11 +7,17 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:JvmName("PlatformSocketKt_common") + package net.mamoe.mirai.internal.utils import io.ktor.utils.io.core.* import io.ktor.utils.io.errors.* +import net.mamoe.mirai.internal.network.handler.SocketAddress +import net.mamoe.mirai.internal.network.handler.getHost +import net.mamoe.mirai.internal.network.handler.getPort import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel +import kotlin.jvm.JvmName /** * TCP Socket. @@ -32,7 +38,6 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel { * @throws ReadPacketInternalException */ override suspend fun read(): ByteReadPacket - suspend fun connect(serverHost: String, serverPort: Int) companion object { suspend fun connect( @@ -48,6 +53,10 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel { } } +internal suspend inline fun PlatformSocket.Companion.connect(address: SocketAddress): PlatformSocket { + return connect(address.getHost(), address.getPort()) +} + internal expect class SocketException : IOException { constructor() diff --git a/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt b/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt deleted file mode 100644 index 112e81e42..000000000 --- a/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt +++ /dev/null @@ -1,624 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -@file:Suppress("DEPRECATION", "OverridingDeprecatedMember") - -package net.mamoe.mirai.internal.utils - -import io.ktor.utils.io.core.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.runBlocking -import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.isOperator -import net.mamoe.mirai.internal.asQQAndroidBot -import net.mamoe.mirai.internal.contact.groupCode -import net.mamoe.mirai.internal.message.data.FileMessageImpl -import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag -import net.mamoe.mirai.internal.network.highway.Highway -import net.mamoe.mirai.internal.network.highway.ResourceKind -import net.mamoe.mirai.internal.network.protocol -import net.mamoe.mirai.internal.network.protocol.data.proto.* -import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement -import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult -import net.mamoe.mirai.internal.utils.io.serialization.toByteArray -import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.FileMessage -import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH -import kotlin.contracts.contract -import kotlin.jvm.Volatile - -private val fs = FileSystem - -// internal for tests -internal object FileSystem { - fun checkLegitimacy(path: String) { - val char = path.firstOrNull { it in """:*?"<>|""" } - if (char != null) { - throw IllegalArgumentException("""Chars ':*?"<>|' are not allowed in path. RemoteFile path contains illegal char: '$char'. path='$path'""") - } - } - - fun isLegal(path: String): Boolean { - return path.firstOrNull { it in """:*?"<>|""" } == null - } - - fun normalize(path: String): String { - checkLegitimacy(path) - return path.replace('\\', '/') - } - - // net.mamoe.mirai.internal.utils.internal.utils.FileSystemTest - - fun normalize(parent: String, name: String): String { - var nName = normalize(name) - if (nName.startsWith('/')) return nName // absolute path then ignore parent - nName = nName.removeSuffix("/") - - var nParent = normalize(parent) - if (nParent == "/") return "/$nName" - if (!nParent.startsWith('/')) nParent = "/$nParent" - - val slash = nName.indexOf('/') - if (slash != -1) { - nParent += '/' + nName.substring(0, slash) - nName = nName.substring(slash + 1) - } - - return "$nParent/$nName" - } -} - -internal class RemoteFileInfo( - val id: String, // fileId or folderId - val isFile: Boolean, - val path: String, - val name: String, - val parentFolderId: String, - val size: Long, - val busId: Int, // for file only - val creatorId: Long, //ownerUin, createUin - val createTime: Long, // uploadTime, createTime - val modifyTime: Long, - val downloadTimes: Int, - val sha: ByteArray, // for file only - val md5: ByteArray, // for file only -) { - companion object { - val root = RemoteFileInfo( - "/", false, "/", "/", "", 0, 0, 0, 0, 0, 0, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY - ) - } -} - -internal fun RemoteFile.checkIsImpl(): CommonRemoteFileImpl { - contract { returns() implies (this@checkIsImpl is RemoteFileImpl) } - return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.") -} - -internal expect class RemoteFileImpl( - contact: Group, - path: String, // absolute -) : CommonRemoteFileImpl { - constructor(contact: Group, parent: String, name: String) -} - -internal abstract class CommonRemoteFileImpl( - override val contact: Group, - override val path: String, // absolute -) : RemoteFile { - - override var id: String? = null - - override val name: String - get() = path.substringAfterLast('/') - - private val bot get() = contact.bot.asQQAndroidBot() - private val client get() = bot.client - - override val parent: CommonRemoteFileImpl? - get() { - if (path == ROOT_PATH) return null - val s = path.substringBeforeLast('/') - return RemoteFileImpl(contact, s.ifEmpty { ROOT_PATH }) - } - - /** - * Prefer id matching. - */ - private suspend fun Flow.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? { - var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null - - val idMatching = firstOrNull { - if (it.name == this@CommonRemoteFileImpl.name) { - nameMatching = it - } - it.id == this@CommonRemoteFileImpl.id - } - - return idMatching ?: nameMatching - } - - private suspend fun getFileFolderInfo(): RemoteFileInfo? { - val parent = parent ?: return RemoteFileInfo.root - val info = parent.getFilesFlow() - .filter { it.name == this.name } - .findMatching() - ?: return null - return when { - info.folderInfo != null -> info.folderInfo.run { - RemoteFileInfo( - id = folderId, - isFile = false, - path = path, - name = folderName, - parentFolderId = parentFolderId, - size = 0, - busId = 0, - creatorId = createUin, - createTime = createTime.toLongUnsigned(), - modifyTime = modifyTime.toLongUnsigned(), - downloadTimes = 0, - sha = EMPTY_BYTE_ARRAY, - md5 = EMPTY_BYTE_ARRAY, - ) - } - info.fileInfo != null -> info.fileInfo.run { - RemoteFileInfo( - id = fileId, - isFile = true, - path = path, - name = fileName, - parentFolderId = parentFolderId, - size = fileSize, - busId = busId, - creatorId = uploaderUin, - createTime = uploadTime.toLongUnsigned(), - modifyTime = modifyTime.toLongUnsigned(), - downloadTimes = downloadTimes, - sha = sha, - md5 = md5, - ) - } - else -> null - } - } - - private fun RemoteFileInfo?.checkExists(thisPath: String, kind: String = "Remote path"): RemoteFileInfo { - if (this == null) throw IllegalStateException("$kind '$thisPath' does not exist.") - return this - } - - override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile - - // compiler bug - override suspend fun isDirectory(): Boolean = !isFile() - override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size - override suspend fun exists(): Boolean = this.getFileFolderInfo() != null - override suspend fun getInfo(): RemoteFile.FileInfo? { - return getFileFolderInfo()?.run { - RemoteFile.FileInfo( - name = name, - id = id, - path = path, - length = size, - downloadTimes = downloadTimes, - uploaderId = creatorId, - uploadTime = createTime, - lastModifyTime = modifyTime, - sha1 = sha, - md5 = md5, - ) - } - } - - private suspend fun getFilesFlow(): Flow { - val info = getFileFolderInfo() ?: return emptyFlow() - - return flow { - var index = 0 - while (true) { - val list = bot.network.sendAndExpect( - FileManagement.GetFileList( - client, - groupCode = contact.id, - folderId = info.id, - startIndex = index - ) - ).toResult("RemoteFile.listFiles").getOrThrow() - index += list.itemList.size - - if (list.int32RetCode != 0) return@flow - if (list.itemList.isEmpty()) return@flow - - emitAll(list.itemList.asFlow()) - } - } - } - - private fun Oidb0x6d8.GetFileListRspBody.Item.resolveToFile(): RemoteFile? { - val item = this - return when { - item.fileInfo != null -> { - resolve(item.fileInfo.fileName) - } - item.folderInfo != null -> { - resolve(item.folderInfo.folderName) - } - else -> null - }?.also { - it.id = item.id - } - } - - override suspend fun listFiles(): Flow { - return getFilesFlow().mapNotNull { item -> - item.resolveToFile() - } - } - - // compiler bug - override suspend fun listFilesCollection(): List = listFiles().toList() - - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @OptIn(JavaFriendlyAPI::class) - override suspend fun listFilesIterator(lazy: Boolean): Iterator { - if (!lazy) return listFiles().toList().iterator() - - return object : Iterator { - private val queue = ArrayDeque(1) - - @Volatile - private var index = 0 - private var ended = false - - private suspend fun updateItems() { - val list = bot.network.sendAndExpect( - FileManagement.GetFileList( - client, - groupCode = contact.id, - folderId = path, - startIndex = index - ) - ).toResult("RemoteFile.listFiles").getOrThrow() - if (list.int32RetCode != 0 || list.itemList.isEmpty()) { - ended = true - return - } - index += list.itemList.size - for (item in list.itemList) { - if (item.fileInfo != null || item.folderInfo != null) queue.add(item) - } - } - - override fun hasNext(): Boolean { - if (queue.isEmpty() && !ended) runBlocking { updateItems() } - return queue.isNotEmpty() - } - - override fun next(): RemoteFile { - return queue.removeFirst().resolveToFile()!! - } - } - } - - override fun resolve(relative: String) = RemoteFileImpl(contact, this.path, relative) - override fun resolve(relative: RemoteFile): RemoteFileImpl { - if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.") - - return resolve(relative.path).also { it.id = relative.id } - } - - override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? { - if (this.id == id) return this - val dirs = mutableListOf() - getFilesFlow().mapNotNull { item -> - when { - item.id == id -> item.resolveToFile() - deep && item.folderInfo != null -> { - dirs.add(item) - null - } - else -> null - } - }.firstOrNull()?.let { return it } - for (dir in dirs) { - dir.resolveToFile()?.resolveById(id, deep)?.let { return it } - } - return null - } - - // compiler bug - override suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true) - - override fun resolveSibling(relative: String): RemoteFileImpl { - val parent = this.parent - if (parent == null) { - if (fs.normalize(relative) == ROOT_PATH) error("Root path does not have sibling paths.") - return RemoteFileImpl(contact, ROOT_PATH) - } - return RemoteFileImpl(contact, parent.path, relative) - } - - override fun resolveSibling(relative: RemoteFile): RemoteFileImpl { - if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.") - - return resolveSibling(relative.path).also { it.id = relative.id } - } - - private fun RemoteFileInfo.isOperable(): Boolean = - creatorId == bot.id || contact.botPermission.isOperator() - - private fun isBotOperator(): Boolean = contact.botPermission.isOperator() - - override suspend fun delete(): Boolean { - val info = getFileFolderInfo() ?: return false - if (!info.isOperable()) return false - return when { - info.isFile -> { - bot.network.sendAndExpect( - FileManagement.DeleteFile( - client, - groupCode = contact.id, - busId = info.busId, - fileId = info.id, - parentFolderId = info.parentFolderId, - ) - ).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0 - } - // recursively -> { - // this.listFiles().collect { child -> - // child.delete() - // } - // this.delete() - // } - else -> { - // natively 'recursive' - bot.network.sendAndExpect( - FileManagement.DeleteFolder( - client, contact.id, info.id - ) - ).toResult("RemoteFile.delete").getOrThrow().int32RetCode == 0 - } - } - } - - override suspend fun renameTo(name: String): Boolean { - if (path == ROOT_PATH && name != ROOT_PATH) return false - - val normalized = fs.normalize(name) - if (normalized.contains('/')) throw IllegalArgumentException("'/' is not allowed in file or directory names. Given: '$name'.") - - val info = getFileFolderInfo() ?: return false - if (!info.isOperable()) return false - return bot.network.sendAndExpect( - if (info.isFile) { - FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized) - } else { - FileManagement.RenameFolder(client, contact.id, info.id, normalized) - } - ).toResult("RemoteFile.renameTo", checkResp = false).getOrThrow().int32RetCode == 0 - } - - /** - * null means not exist - */ - private suspend fun getIdSmart(): String? { - if (path == ROOT_PATH) return ROOT_PATH - return this.id ?: this.getFileFolderInfo()?.id - } - - override suspend fun moveTo(target: RemoteFile): Boolean { - if (target.checkIsImpl().contact != this.contact) { - // TODO: 2021/3/4 cross-group file move - - // target.mkdir() - // val targetFolderId = target.getIdSmart() ?: return false - // this.listFiles().mapNotNull { it.checkIsImpl().getFileFolderInfo() }.collect { - // FileManagement.MoveFile(client, contact.id, it.busId, it.id, it.parentFolderId, targetFolderId) - // .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow() - // - // // TODO: 2021/3/3 batch packets - // } - // this.delete() // it is now empty - - error("Cross-group file operation is not yet supported.") - } - if (target.path == this.path) return true - if (target.parent?.path == this.path) return false - val info = getFileFolderInfo() ?: return false - if (!info.isOperable()) return false - return if (info.isFile) { - val newParentId = target.parent?.checkIsImpl()?.getIdSmart() ?: return false - bot.network.sendAndExpect( - FileManagement.MoveFile( - client, - contact.id, - info.busId, - info.id, - info.parentFolderId, - newParentId - ) - ).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 - } else { - return bot.network.sendAndExpect(FileManagement.RenameFolder(client, contact.id, info.id, target.name)) - .toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 - } - } - - - override suspend fun mkdir(): Boolean { - if (path == ROOT_PATH) return false - if (!isBotOperator()) return false - - val parentFolderId: String = parent?.getIdSmart() ?: return false - - return bot.network.sendAndExpect(FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name)) - .toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0 - } - - private suspend fun upload0( - resource: ExternalResource, - callback: RemoteFile.ProgressionCallback?, - ): Oidb0x6d6.UploadFileRspBody? = resource.withAutoClose { - val parent = parent ?: return null - val parentInfo = parent.getFileFolderInfo() ?: return null - val resp = bot.network.sendAndExpect( - FileManagement.RequestUpload( - client, - groupCode = contact.id, - folderId = parentInfo.id, - resource = resource, - filename = this.name - ) - ).toResult("RemoteFile.upload").getOrThrow() - if (resp.boolFileExist) { - return resp - } - - val ext = GroupFileUploadExt( - u1 = 100, - u2 = 1, - entry = GroupFileUploadEntry( - business = ExcitingBusiInfo( - busId = resp.busId, - senderUin = bot.id, - receiverUin = contact.groupCode, // TODO: 2021/3/1 code or uin? - groupCode = contact.groupCode, - ), - fileEntry = ExcitingFileEntry( - fileSize = resource.size, - md5 = resource.md5, - sha1 = resource.sha1, - fileId = resp.fileId.toByteArray(), - uploadKey = resp.checkKey, - ), - clientInfo = ExcitingClientInfo( - clientType = 2, - appId = client.protocol.id.toString(), - terminalType = 2, - clientVer = "9e9c09dc", - unknown = 4, - ), - fileNameInfo = ExcitingFileNameInfo(this.name), - host = ExcitingHostConfig( - hosts = listOf( - ExcitingHostInfo( - url = ExcitingUrlInfo( - unknown = 1, - host = resp.uploadIpLanV4.firstOrNull() - ?: resp.uploadIpLanV6.firstOrNull() - ?: resp.uploadIp, - ), - port = resp.uploadPort, - ), - ), - ), - ), - u3 = 0, - ).toByteArray(GroupFileUploadExt.serializer()) - - callback?.onBegin(this, resource) - - kotlin.runCatching { - Highway.uploadResourceBdh( - bot = bot, - resource = resource, - kind = ResourceKind.GROUP_FILE, - commandId = 71, - extendInfo = ext, - dataFlag = 0, - callback = if (callback == null) null else fun(it: Long) { - callback.onProgression(this, resource, it) - } - ) - }.fold( - onSuccess = { - callback?.onSuccess(this, resource) - }, - onFailure = { - callback?.onFailure(this, resource, it) - } - ) - - return resp - } - - private suspend fun uploadInternal( - resource: ExternalResource, - callback: RemoteFile.ProgressionCallback?, - ): FileMessage { - val resp = upload0(resource, callback) ?: error("Failed to upload file.") - return FileMessageImpl( - resp.fileId, resp.busId, name, resource.size, allowSend = true - ) - } - - @Deprecated( - "Use uploadAndSend instead.", - replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"), - level = DeprecationLevel.ERROR - ) - override suspend fun upload( - resource: ExternalResource, - callback: RemoteFile.ProgressionCallback?, - ): FileMessage { - val msg = uploadInternal(resource, callback) - contact.sendMessage(msg + MiraiInternalMessageFlag) - return msg - } - - // compiler bug - @Deprecated( - "Use uploadAndSend instead.", - replaceWith = ReplaceWith("this.uploadAndSend(resource)"), - level = DeprecationLevel.ERROR - ) - @Suppress("DEPRECATION_ERROR") - override suspend fun upload(resource: ExternalResource): FileMessage { - return upload(resource, null) - } - - override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt { - @Suppress("DEPRECATION") - return contact.sendMessage(uploadInternal(resource, null) + MiraiInternalMessageFlag) - } - - override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? { - val info = getFileFolderInfo() ?: return null - if (!info.isFile) return null - val resp = bot.network.sendAndExpect( - FileManagement.RequestDownload( - client, - groupCode = contact.id, - busId = info.busId, - fileId = info.id - ) - ).toResult("RemoteFile.getDownloadInfo").getOrThrow() - - return RemoteFile.DownloadInfo( - filename = name, - id = info.id, - path = path, - url = "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" + - info.id.toByteArray().toUHexString(""), - sha1 = info.sha, - md5 = info.md5 - ) - } - - override fun toString(): String = path - - override suspend fun toMessage(): FileMessage? { - val info = getFileFolderInfo() ?: return null - if (!info.isFile) return null - return FileMessageImpl(info.id, info.busId, name, info.size) - } -} diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt index 8c684b0e8..c546f2614 100644 --- a/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt +++ b/mirai-core/src/commonMain/kotlin/utils/crypto/ECDH.kt @@ -15,9 +15,7 @@ import net.mamoe.mirai.utils.hexToBytes internal expect interface ECDHPrivateKey -internal expect interface ECDHPublicKey { - fun getEncoded(): ByteArray -} +internal expect interface ECDHPublicKey internal expect class ECDHKeyPairImpl : ECDHKeyPair @@ -48,9 +46,6 @@ internal interface ECDHKeyPair { } } -/** - * 椭圆曲线密码, ECDH 加密 - */ internal expect class ECDH(keyPair: ECDHKeyPair) { val keyPair: ECDHKeyPair @@ -62,8 +57,11 @@ internal expect class ECDH(keyPair: ECDHKeyPair) { companion object { val isECDHAvailable: Boolean + /** - * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey] + * This API is platform dependent. + * On JVM you need to add `signHead`, + * but on Native you need to provide a key with initial byte value 0x04 and of 65 bytes' length. */ fun constructPublicKey(key: ByteArray): ECDHPublicKey @@ -78,7 +76,7 @@ internal expect class ECDH(keyPair: ECDHKeyPair) { fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair /** - * 由一对密匙计算 shareKey + * 由一对密匙计算服务器需要的 shareKey */ fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray } @@ -119,19 +117,12 @@ internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialP @Serializable internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) { @Transient - internal val key: ECDHPublicKey = keyStr.adjustToPublicKey() + internal val key: ECDHPublicKey = keyStr.hexToBytes().adjustToPublicKey() } -internal expect val publicKeyForVerify: ECDHPublicKey - internal val defaultInitialPublicKey: ECDHInitialPublicKey by lazy { ECDHInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E") } -private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes() -internal fun String.adjustToPublicKey(): ECDHPublicKey { - return this.hexToBytes().adjustToPublicKey() -} -internal fun ByteArray.adjustToPublicKey(): ECDHPublicKey { +internal expect fun ByteArray.adjustToPublicKey(): ECDHPublicKey - return ECDH.constructPublicKey(signHead + this) -} \ No newline at end of file +internal val ECDH.Companion.curveName get() = "prime256v1" // p-256 diff --git a/mirai-core/src/commonTest/kotlin/event/EventChannelFlowTest.kt b/mirai-core/src/commonTest/kotlin/event/EventChannelFlowTest.kt index 1e5451514..09f835d4f 100644 --- a/mirai-core/src/commonTest/kotlin/event/EventChannelFlowTest.kt +++ b/mirai-core/src/commonTest/kotlin/event/EventChannelFlowTest.kt @@ -12,14 +12,12 @@ package net.mamoe.mirai.internal.event import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.async import kotlinx.coroutines.flow.first -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.internal.test.runBlockingUnit import kotlin.test.Test import kotlin.test.assertIs -@JvmBlockingBridge internal class EventChannelFlowTest : AbstractEventTest() { @Test diff --git a/mirai-core/src/commonTest/kotlin/event/EventChannelTest.kt b/mirai-core/src/commonTest/kotlin/event/EventChannelTest.kt index dc68efe2d..ac9096f9e 100644 --- a/mirai-core/src/commonTest/kotlin/event/EventChannelTest.kt +++ b/mirai-core/src/commonTest/kotlin/event/EventChannelTest.kt @@ -21,7 +21,6 @@ import net.mamoe.mirai.event.events.MessageEvent import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine import kotlin.test.* internal class EventChannelTest : AbstractEventTest() { @@ -37,7 +36,7 @@ internal class EventChannelTest : AbstractEventTest() { @Test fun singleFilter() { runBlocking { - val received = suspendCoroutine { cont -> + val received = suspendCancellableCoroutine { cont -> globalEventChannel() .filterIsInstance() .filter { @@ -69,7 +68,7 @@ internal class EventChannelTest : AbstractEventTest() { @Test fun multipleFilters() { runBlocking { - val received = suspendCoroutine { cont -> + val received = suspendCancellableCoroutine { cont -> globalEventChannel() .filterIsInstance() .filter { @@ -109,7 +108,7 @@ internal class EventChannelTest : AbstractEventTest() { fun multipleContexts1() { runBlocking { withContext(CoroutineName("1")) { - val received = suspendCoroutine { cont -> + val received = suspendCancellableCoroutine { cont -> globalEventChannel() .context(CoroutineName("2")) .context(CoroutineName("3")) @@ -132,7 +131,7 @@ internal class EventChannelTest : AbstractEventTest() { fun multipleContexts2() { runBlocking { withContext(CoroutineName("1")) { - val received = suspendCoroutine { cont -> + val received = suspendCancellableCoroutine { cont -> globalEventChannel() .context(CoroutineName("2")) .context(CoroutineName("3")) @@ -156,7 +155,7 @@ internal class EventChannelTest : AbstractEventTest() { fun multipleContexts3() { runBlocking { withContext(CoroutineName("1")) { - val received = suspendCoroutine { cont -> + val received = suspendCancellableCoroutine { cont -> globalEventChannel() .context(CoroutineName("2")) .subscribeOnce { @@ -178,7 +177,7 @@ internal class EventChannelTest : AbstractEventTest() { fun multipleContexts4() { runBlocking { withContext(CoroutineName("1")) { - val received = suspendCoroutine { cont -> + val received = suspendCancellableCoroutine { cont -> globalEventChannel() .subscribeOnce { assertEquals("1", currentCoroutineContext()[CoroutineName]!!.name) @@ -250,7 +249,8 @@ internal class EventChannelTest : AbstractEventTest() { fun testExceptionInFilter() { assertFailsWith { runBlocking { - suspendCoroutine { cont -> + @Suppress("RemoveExplicitTypeArguments") + suspendCancellableCoroutine { cont -> globalEventChannel() .exceptionHandler { cont.resumeWithException(it) @@ -278,7 +278,7 @@ internal class EventChannelTest : AbstractEventTest() { fun testExceptionInSubscribe() { runBlocking { assertFailsWith { - suspendCoroutine { cont -> + suspendCancellableCoroutine { cont -> val handler = CoroutineExceptionHandler { _, throwable -> cont.resumeWithException(throwable) } diff --git a/mirai-core/src/commonTest/kotlin/event/EventTests.kt b/mirai-core/src/commonTest/kotlin/event/EventTests.kt index ac3b52610..3d33eb1b2 100644 --- a/mirai-core/src/commonTest/kotlin/event/EventTests.kt +++ b/mirai-core/src/commonTest/kotlin/event/EventTests.kt @@ -60,10 +60,11 @@ internal class EventTests : AbstractEventTest() { resetEventListeners() var listeners = 0 val counter = atomic(0) + val channel = scope.globalEventChannel() for (p in EventPriority.values()) { repeat(2333) { listeners++ - scope.globalEventChannel().subscribeAlways { + channel.subscribeAlways { counter.getAndIncrement() } } diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt index 0aed8969b..004ffbc2c 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt @@ -21,21 +21,22 @@ internal class MessageProtocolFacadeTest : AbstractTest() { assertEquals( """ QuoteReplyProtocol + AudioProtocol CustomMessageProtocol + FaceProtocol FileMessageProtocol FlashImageProtocol - FaceProtocol ImageProtocol MarketFaceProtocol MusicShareProtocol PokeMessageProtocol - IgnoredMessagesProtocol PttMessageProtocol RichMessageProtocol TextProtocol VipFaceProtocol ForwardMessageProtocol LongMessageProtocol + IgnoredMessagesProtocol UnsupportedMessageProtocol GeneralMessageSenderProtocol """.trimIndent(), diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt index 84f93acae..be127ce14 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt @@ -18,6 +18,7 @@ import net.mamoe.mirai.message.data.MessageSource.Key.quote import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.QuoteReply import net.mamoe.mirai.message.data.messageChainOf +import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY import net.mamoe.mirai.utils.hexToBytes import kotlin.test.Test @@ -86,6 +87,7 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() { ), ), ), + srcMsg = EMPTY_BYTE_ARRAY // mirai's OfflineMessageSource has no enough information to create 'srcMsg' ), ), @@ -319,6 +321,7 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() { ), ), ), + srcMsg = EMPTY_BYTE_ARRAY // mirai's OfflineMessageSource has no enough information to create 'srcMsg' ), ), diff --git a/mirai-core/src/commonTest/kotlin/network/component/AbstractMutableComponentStorageTest.kt b/mirai-core/src/commonTest/kotlin/network/component/AbstractMutableComponentStorageTest.kt index 972262d58..78f44d267 100644 --- a/mirai-core/src/commonTest/kotlin/network/component/AbstractMutableComponentStorageTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/component/AbstractMutableComponentStorageTest.kt @@ -14,6 +14,19 @@ import kotlin.test.Test import kotlin.test.assertEquals internal abstract class AbstractMutableComponentStorageTest : AbstractTest() { + + internal data class TestComponent2( + val value: Int + ) { + companion object : ComponentKey + } + + internal data class TestComponent3( + val value: Int + ) { + companion object : ComponentKey + } + protected abstract fun createStorage(): MutableComponentStorage @Test diff --git a/mirai-core/src/commonTest/kotlin/network/component/CombinedStorageTest.kt b/mirai-core/src/commonTest/kotlin/network/component/CombinedStorageTest.kt index 8a37c9732..0ff413ca8 100644 --- a/mirai-core/src/commonTest/kotlin/network/component/CombinedStorageTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/component/CombinedStorageTest.kt @@ -7,15 +7,30 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:OptIn(TestOnly::class) + package net.mamoe.mirai.internal.network.component import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.utils.TestOnly import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame internal class CombinedStorageTest : AbstractTest() { + internal data class TestComponent2( + val value: Int + ) { + companion object : ComponentKey + } + + internal data class TestComponent3( + val value: Int + ) { + companion object : ComponentKey + } + @Test fun `can get from main`() { val storage = ConcurrentComponentStorage().apply { diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt index f4828ea71..b85710329 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt @@ -24,11 +24,8 @@ import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage import net.mamoe.mirai.internal.network.component.setAll import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.framework.components.TestSsoProcessor -import net.mamoe.mirai.internal.network.handler.NetworkHandler +import net.mamoe.mirai.internal.network.handler.* import net.mamoe.mirai.internal.network.handler.NetworkHandler.State -import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl -import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory -import net.mamoe.mirai.internal.network.handler.SocketAddress import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.utils.subLogger @@ -175,7 +172,7 @@ internal abstract class AbstractRealNetworkHandlerTest : Abs //Use overrideComponents to avoid StackOverflowError when applying components open fun createAddress(): SocketAddress = - overrideComponents[ServerList].pollAny().let { SocketAddress(it.host, it.port) } + overrideComponents[ServerList].pollAny().let { createSocketAddress(it.host, it.port) } /////////////////////////////////////////////////////////////////////////// // Assertions diff --git a/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt b/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt index 0deb61695..cf77ad316 100644 --- a/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt @@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.test.runBlockingUnit import net.mamoe.mirai.utils.TestOnly import kotlin.test.Test import kotlin.test.assertFails +import kotlin.test.assertTrue /** * Test whether the selector can recover the connection after first successful login. @@ -55,7 +56,7 @@ internal class SelectorRecoveryTest : AbstractCommonNHTestWithSelector() { bot.components[EventDispatcher].joinBroadcast() // Wait our async connector to complete. // BotOfflineMonitor immediately launches a recovery which is UNDISPATCHED, so connection is immediately recovered. - assertState(NetworkHandler.State.CONNECTING, NetworkHandler.State.LOADING, NetworkHandler.State.OK) + assertTrue { bot.network.state != NetworkHandler.State.CLOSED } } @Test diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt index a34307fb2..2f3899601 100644 --- a/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt +++ b/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt @@ -19,10 +19,7 @@ import net.mamoe.mirai.event.events.GroupMessageEvent import net.mamoe.mirai.event.events.GroupTempMessageEvent import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC import net.mamoe.mirai.internal.test.runBlockingUnit -import net.mamoe.mirai.message.data.MessageSource -import net.mamoe.mirai.message.data.OnlineMessageSource -import net.mamoe.mirai.message.data.PlainText -import net.mamoe.mirai.message.data.content +import net.mamoe.mirai.message.data.* import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @@ -124,7 +121,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() { assertEquals(1630, time) assertEquals(1230001, fromId) assertEquals(2230203, targetId) - assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage) } assertIs(get(1)) assertEquals("hello", get(1).content) @@ -205,7 +202,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() { assertEquals(1630, time) assertEquals(1230001, fromId) assertEquals(1230003, targetId) - assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage) } assertIs<PlainText>(get(1)) assertEquals("123", get(1).content) @@ -298,7 +295,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() { assertEquals(1630, time) assertEquals(1230001, fromId) assertEquals(1230003, targetId) - assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage) } assertIs<PlainText>(get(1)) assertEquals("hello", get(1).content) @@ -392,7 +389,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() { assertEquals(1630, time) assertEquals(1230001, fromId) assertEquals(1230003, targetId) - assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage) } assertIs<PlainText>(get(1)) assertEquals("hello", get(1).content) diff --git a/mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt b/mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt new file mode 100644 index 000000000..81662f7f4 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.crypto + +import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.utils.toUHexString +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +internal class ECDHTest : AbstractTest() { + + @Test + fun `can generate key pair`() { + val alice = ECDH.generateKeyPair() + val bob = ECDH.generateKeyPair() + + val aliceSecret = ECDH.calculateShareKey(alice.privateKey, bob.publicKey) + val bobSecret = ECDH.calculateShareKey(bob.privateKey, alice.publicKey) + + println(aliceSecret.toUHexString()) + assertContentEquals(aliceSecret, bobSecret) + } + + @Test + fun `can get masked keys`() { + val alice = ECDH.generateKeyPair() + + println(alice) + val maskedPublicKey = alice.maskedPublicKey + println(maskedPublicKey.toUHexString()) + assertEquals(0x04, maskedPublicKey.first()) + println(alice.maskedShareKey.toUHexString()) + } + + /* + + EC_KEY *alice = create_key(); + EC_KEY *bob = create_key(); + assert(alice != NULL && bob != NULL); + + const EC_POINT *alice_public = EC_KEY_get0_public_key(alice); + const EC_POINT *bob_public = EC_KEY_get0_public_key(bob); + + size_t alice_secret_len; + size_t bob_secret_len; + + unsigned char *alice_secret = get_secret(alice, bob_public, &alice_secret_len); + unsigned char *bob_secret = get_secret(bob, alice_public, &bob_secret_len); + assert(alice_secret != NULL && bob_secret != NULL + && alice_secret_len == bob_secret_len); + + for (int i = 0; i < alice_secret_len; i++) + assert(alice_secret[i] == bob_secret[i]); + + EC_KEY_free(alice); + EC_KEY_free(bob); + OPENSSL_free(alice_secret); + OPENSSL_free(bob_secret); + + return 0; + */ +} \ No newline at end of file diff --git a/mirai-core/src/darwinMain/kotlin/MiraiImpl.kt b/mirai-core/src/darwinMain/kotlin/MiraiImpl.kt new file mode 100644 index 000000000..e55c4c34d --- /dev/null +++ b/mirai-core/src/darwinMain/kotlin/MiraiImpl.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal + +import io.ktor.client.* +import io.ktor.client.engine.ios.* +import io.ktor.client.features.* + +internal actual fun createDefaultHttpClient(): HttpClient { + return HttpClient(Ios) { + install(HttpTimeout) { + this.requestTimeoutMillis = 30_0000 + this.connectTimeoutMillis = 30_0000 + this.socketTimeoutMillis = 30_0000 + } + } +} diff --git a/mirai-core/src/darwinMain/kotlin/package.kt b/mirai-core/src/darwinMain/kotlin/package.kt new file mode 100644 index 000000000..7df5cebc4 --- /dev/null +++ b/mirai-core/src/darwinMain/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal \ No newline at end of file diff --git a/mirai-core/src/darwinTest/kotlin/package.kt b/mirai-core/src/darwinTest/kotlin/package.kt new file mode 100644 index 000000000..7df5cebc4 --- /dev/null +++ b/mirai-core/src/darwinTest/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/MiraiImpl.kt b/mirai-core/src/jvmBaseMain/kotlin/MiraiImpl.kt new file mode 100644 index 000000000..286f18881 --- /dev/null +++ b/mirai-core/src/jvmBaseMain/kotlin/MiraiImpl.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +@file:JvmName("MiraiImplKt") + +package net.mamoe.mirai.internal + +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.features.* + +@Suppress("FunctionName") +internal actual fun _MiraiImpl_static_init() { + // nop +} + +internal actual fun createDefaultHttpClient(): HttpClient { + return HttpClient(OkHttp) { + install(HttpTimeout) { + this.requestTimeoutMillis = 30_0000 + this.connectTimeoutMillis = 30_0000 + this.socketTimeoutMillis = 30_0000 + } + } + +} + diff --git a/mirai-core/src/jvmBaseMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/jvmBaseMain/kotlin/contact/GroupImpl.kt new file mode 100644 index 000000000..e8b70b389 --- /dev/null +++ b/mirai-core/src/jvmBaseMain/kotlin/contact/GroupImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.contact + +import net.mamoe.mirai.contact.ContactList +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.data.GroupInfo +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.utils.RemoteFileImpl +import net.mamoe.mirai.utils.DeprecatedSinceMirai +import net.mamoe.mirai.utils.RemoteFile +import kotlin.coroutines.CoroutineContext + +internal actual class GroupImpl actual constructor( + bot: QQAndroidBot, + parentCoroutineContext: CoroutineContext, + id: Long, + groupInfo: GroupInfo, + members: ContactList<NormalMemberImpl>, +) : Group, CommonGroupImpl(bot, parentCoroutineContext, id, groupInfo, members) { + actual companion object; + + @Suppress("DEPRECATION") + @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.WARNING) + @DeprecatedSinceMirai(warningSince = "2.8") + override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") } +} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/network/handler/SocketAddress.kt b/mirai-core/src/jvmBaseMain/kotlin/network/handler/SocketAddress.kt index ae732d448..5e692a13a 100644 --- a/mirai-core/src/jvmBaseMain/kotlin/network/handler/SocketAddress.kt +++ b/mirai-core/src/jvmBaseMain/kotlin/network/handler/SocketAddress.kt @@ -12,8 +12,14 @@ package net.mamoe.mirai.internal.network.handler import java.net.InetSocketAddress @Suppress("ACTUAL_WITHOUT_EXPECT") // visibility -internal actual typealias SocketAddress = java.net.SocketAddress +internal actual typealias SocketAddress = java.net.InetSocketAddress -internal actual fun SocketAddress(host: String, port: Int): SocketAddress { +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +internal actual fun SocketAddress.getHost(): String = hostString ?: error("Failed to get host from address '$this'.") + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +internal actual fun SocketAddress.getPort(): Int = this.port + +internal actual fun createSocketAddress(host: String, port: Int): SocketAddress { return InetSocketAddress.createUnresolved(host, port) } \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandler.kt b/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandler.kt index fe7c2d44b..d2e3edce1 100644 --- a/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandler.kt +++ b/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandler.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.internal.network.impl.netty +import io.ktor.utils.io.core.* import io.netty.bootstrap.Bootstrap import io.netty.buffer.ByteBuf import io.netty.channel.* @@ -20,13 +21,11 @@ import io.netty.handler.codec.MessageToByteEncoder import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.job -import net.mamoe.mirai.internal.network.components.PacketCodec -import net.mamoe.mirai.internal.network.components.RawIncomingPacket -import net.mamoe.mirai.internal.network.components.SsoProcessor import net.mamoe.mirai.internal.network.handler.CommonNetworkHandler import net.mamoe.mirai.internal.network.handler.NetworkHandler.State import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.utils.cast import net.mamoe.mirai.utils.debug import java.net.SocketAddress import io.netty.channel.Channel as NettyChannel @@ -34,7 +33,7 @@ import io.netty.channel.Channel as NettyChannel internal open class NettyNetworkHandler( context: NetworkHandlerContext, address: SocketAddress, -) : CommonNetworkHandler<NettyChannel>(context, address) { +) : CommonNetworkHandler<NettyChannel>(context, address.cast()) { override fun toString(): String { return "NettyNetworkHandler(context=$context, address=$address)" } @@ -52,26 +51,11 @@ internal open class NettyNetworkHandler( // netty conn. /////////////////////////////////////////////////////////////////////////// - private inner class ByteBufToIncomingPacketDecoder : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) { - private val packetCodec: PacketCodec by lazy { context[PacketCodec] } - private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] } - - override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { - kotlin.runCatching { - ctx.fireChannelRead(msg.toReadPacket().use { packet -> - packetCodec.decodeRaw(ssoProcessor.ssoSession, packet) - }) - }.onFailure { error -> - handleExceptionInDecoding(error) - } - } - } - - private inner class RawIncomingPacketCollector( + private inner class IncomingPacketDecoder( private val decodePipeline: PacketDecodePipeline, - ) : SimpleChannelInboundHandler<RawIncomingPacket>(RawIncomingPacket::class.java) { - override fun channelRead0(ctx: ChannelHandlerContext, msg: RawIncomingPacket) { - decodePipeline.send(msg) + ) : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) { + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { + decodePipeline.send(msg.toReadPacket()) } } @@ -91,8 +75,7 @@ internal open class NettyNetworkHandler( }) .addLast("outgoing-packet-encoder", OutgoingPacketEncoder()) .addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4)) - .addLast(ByteBufToIncomingPacketDecoder()) - .addLast("raw-packet-collector", RawIncomingPacketCollector(decodePipeline)) + .addLast(IncomingPacketDecoder(decodePipeline)) } protected open fun createDummyDecodePipeline() = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext) diff --git a/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandlerFactory.kt b/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandlerFactory.kt index ec6e5173e..f66b65ae2 100644 --- a/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandlerFactory.kt +++ b/mirai-core/src/jvmBaseMain/kotlin/network/impl/netty/NettyNetworkHandlerFactory.kt @@ -11,7 +11,7 @@ package net.mamoe.mirai.internal.network.impl.netty import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory -import java.net.SocketAddress +import net.mamoe.mirai.internal.network.handler.SocketAddress internal object NettyNetworkHandlerFactory : NetworkHandlerFactory<NettyNetworkHandler> { override fun create(context: NetworkHandlerContext, address: SocketAddress): NettyNetworkHandler { diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/PlatformSocket.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/PlatformSocket.kt index 1909a9364..d484f97da 100644 --- a/mirai-core/src/jvmBaseMain/kotlin/utils/PlatformSocket.kt +++ b/mirai-core/src/jvmBaseMain/kotlin/utils/PlatformSocket.kt @@ -86,7 +86,7 @@ internal actual class PlatformSocket : Closeable, HighwayProtocolChannel { } } - actual suspend fun connect(serverHost: String, serverPort: Int) { + suspend fun connect(serverHost: String, serverPort: Int) { runInterruptible(Dispatchers.IO) { socket = Socket(serverHost, serverPort) readChannel = socket.getInputStream().buffered() diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/RemoteFileImpl.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/RemoteFileImpl.kt index 80be35a52..26a2f6d17 100644 --- a/mirai-core/src/jvmBaseMain/kotlin/utils/RemoteFileImpl.kt +++ b/mirai-core/src/jvmBaseMain/kotlin/utils/RemoteFileImpl.kt @@ -11,13 +11,580 @@ package net.mamoe.mirai.internal.utils +import io.ktor.utils.io.core.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.runBlocking import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.isOperator +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.contact.groupCode +import net.mamoe.mirai.internal.message.data.FileMessageImpl +import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag +import net.mamoe.mirai.internal.network.highway.Highway +import net.mamoe.mirai.internal.network.highway.ResourceKind +import net.mamoe.mirai.internal.network.protocol +import net.mamoe.mirai.internal.network.protocol.data.proto.* +import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement +import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult +import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.FileMessage +import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource -import net.mamoe.mirai.utils.RemoteFile +import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH import java.io.File +import kotlin.contracts.contract +import kotlin.text.toByteArray + +private val fs = FileSystem + +internal class RemoteFileInfo( + val id: String, // fileId or folderId + val isFile: Boolean, + val path: String, + val name: String, + val parentFolderId: String, + val size: Long, + val busId: Int, // for file only + val creatorId: Long, //ownerUin, createUin + val createTime: Long, // uploadTime, createTime + val modifyTime: Long, + val downloadTimes: Int, + val sha: ByteArray, // for file only + val md5: ByteArray, // for file only +) { + companion object { + val root = RemoteFileInfo( + "/", false, "/", "/", "", 0, 0, 0, 0, 0, 0, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY + ) + } +} + +internal fun RemoteFile.checkIsImpl(): CommonRemoteFileImpl { + contract { returns() implies (this@checkIsImpl is RemoteFileImpl) } + return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.") +} + +internal expect class RemoteFileImpl( + contact: Group, + path: String, // absolute +) : CommonRemoteFileImpl { + constructor(contact: Group, parent: String, name: String) +} + +internal abstract class CommonRemoteFileImpl( + override val contact: Group, + override val path: String, // absolute +) : RemoteFile { + + override var id: String? = null + + override val name: String + get() = path.substringAfterLast('/') + + private val bot get() = contact.bot.asQQAndroidBot() + private val client get() = bot.client + + override val parent: CommonRemoteFileImpl? + get() { + if (path == ROOT_PATH) return null + val s = path.substringBeforeLast('/') + return RemoteFileImpl(contact, s.ifEmpty { ROOT_PATH }) + } + + /** + * Prefer id matching. + */ + private suspend fun Flow<Oidb0x6d8.GetFileListRspBody.Item>.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? { + var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null + + val idMatching = firstOrNull { + if (it.name == this@CommonRemoteFileImpl.name) { + nameMatching = it + } + it.id == this@CommonRemoteFileImpl.id + } + + return idMatching ?: nameMatching + } + + private suspend fun getFileFolderInfo(): RemoteFileInfo? { + val parent = parent ?: return RemoteFileInfo.root + val info = parent.getFilesFlow() + .filter { it.name == this.name } + .findMatching() + ?: return null + return when { + info.folderInfo != null -> info.folderInfo.run { + RemoteFileInfo( + id = folderId, + isFile = false, + path = path, + name = folderName, + parentFolderId = parentFolderId, + size = 0, + busId = 0, + creatorId = createUin, + createTime = createTime.toLongUnsigned(), + modifyTime = modifyTime.toLongUnsigned(), + downloadTimes = 0, + sha = EMPTY_BYTE_ARRAY, + md5 = EMPTY_BYTE_ARRAY, + ) + } + info.fileInfo != null -> info.fileInfo.run { + RemoteFileInfo( + id = fileId, + isFile = true, + path = path, + name = fileName, + parentFolderId = parentFolderId, + size = fileSize, + busId = busId, + creatorId = uploaderUin, + createTime = uploadTime.toLongUnsigned(), + modifyTime = modifyTime.toLongUnsigned(), + downloadTimes = downloadTimes, + sha = sha, + md5 = md5, + ) + } + else -> null + } + } + + private fun RemoteFileInfo?.checkExists(thisPath: String, kind: String = "Remote path"): RemoteFileInfo { + if (this == null) throw IllegalStateException("$kind '$thisPath' does not exist.") + return this + } + + override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile + + // compiler bug + override suspend fun isDirectory(): Boolean = !isFile() + override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size + override suspend fun exists(): Boolean = this.getFileFolderInfo() != null + override suspend fun getInfo(): RemoteFile.FileInfo? { + return getFileFolderInfo()?.run { + RemoteFile.FileInfo( + name = name, + id = id, + path = path, + length = size, + downloadTimes = downloadTimes, + uploaderId = creatorId, + uploadTime = createTime, + lastModifyTime = modifyTime, + sha1 = sha, + md5 = md5, + ) + } + } + + private suspend fun getFilesFlow(): Flow<Oidb0x6d8.GetFileListRspBody.Item> { + val info = getFileFolderInfo() ?: return emptyFlow() + + return flow { + var index = 0 + while (true) { + val list = bot.network.sendAndExpect( + FileManagement.GetFileList( + client, + groupCode = contact.id, + folderId = info.id, + startIndex = index + ) + ).toResult("RemoteFile.listFiles").getOrThrow() + index += list.itemList.size + + if (list.int32RetCode != 0) return@flow + if (list.itemList.isEmpty()) return@flow + + emitAll(list.itemList.asFlow()) + } + } + } + + private fun Oidb0x6d8.GetFileListRspBody.Item.resolveToFile(): RemoteFile? { + val item = this + return when { + item.fileInfo != null -> { + resolve(item.fileInfo.fileName) + } + item.folderInfo != null -> { + resolve(item.folderInfo.folderName) + } + else -> null + }?.also { + it.id = item.id + } + } + + override suspend fun listFiles(): Flow<RemoteFile> { + return getFilesFlow().mapNotNull { item -> + item.resolveToFile() + } + } + + // compiler bug + override suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList() + + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @OptIn(JavaFriendlyAPI::class) + override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> { + if (!lazy) return listFiles().toList().iterator() + + return object : Iterator<RemoteFile> { + private val queue = ArrayDeque<Oidb0x6d8.GetFileListRspBody.Item>(1) + + @Volatile + private var index = 0 + private var ended = false + + private suspend fun updateItems() { + val list = bot.network.sendAndExpect( + FileManagement.GetFileList( + client, + groupCode = contact.id, + folderId = path, + startIndex = index + ) + ).toResult("RemoteFile.listFiles").getOrThrow() + if (list.int32RetCode != 0 || list.itemList.isEmpty()) { + ended = true + return + } + index += list.itemList.size + for (item in list.itemList) { + if (item.fileInfo != null || item.folderInfo != null) queue.add(item) + } + } + + override fun hasNext(): Boolean { + if (queue.isEmpty() && !ended) runBlocking { updateItems() } + return queue.isNotEmpty() + } + + override fun next(): RemoteFile { + return queue.removeFirst().resolveToFile()!! + } + } + } + + override fun resolve(relative: String) = RemoteFileImpl(contact, this.path, relative) + override fun resolve(relative: RemoteFile): RemoteFileImpl { + if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.") + + return resolve(relative.path).also { it.id = relative.id } + } + + override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? { + if (this.id == id) return this + val dirs = mutableListOf<Oidb0x6d8.GetFileListRspBody.Item>() + getFilesFlow().mapNotNull { item -> + when { + item.id == id -> item.resolveToFile() + deep && item.folderInfo != null -> { + dirs.add(item) + null + } + else -> null + } + }.firstOrNull()?.let { return it } + for (dir in dirs) { + dir.resolveToFile()?.resolveById(id, deep)?.let { return it } + } + return null + } + + // compiler bug + override suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true) + + override fun resolveSibling(relative: String): RemoteFileImpl { + val parent = this.parent + if (parent == null) { + if (fs.normalize(relative) == ROOT_PATH) error("Root path does not have sibling paths.") + return RemoteFileImpl(contact, ROOT_PATH) + } + return RemoteFileImpl(contact, parent.path, relative) + } + + override fun resolveSibling(relative: RemoteFile): RemoteFileImpl { + if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.") + + return resolveSibling(relative.path).also { it.id = relative.id } + } + + private fun RemoteFileInfo.isOperable(): Boolean = + creatorId == bot.id || contact.botPermission.isOperator() + + private fun isBotOperator(): Boolean = contact.botPermission.isOperator() + + override suspend fun delete(): Boolean { + val info = getFileFolderInfo() ?: return false + if (!info.isOperable()) return false + return when { + info.isFile -> { + bot.network.sendAndExpect( + FileManagement.DeleteFile( + client, + groupCode = contact.id, + busId = info.busId, + fileId = info.id, + parentFolderId = info.parentFolderId, + ) + ).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0 + } + // recursively -> { + // this.listFiles().collect { child -> + // child.delete() + // } + // this.delete() + // } + else -> { + // natively 'recursive' + bot.network.sendAndExpect( + FileManagement.DeleteFolder( + client, contact.id, info.id + ) + ).toResult("RemoteFile.delete").getOrThrow().int32RetCode == 0 + } + } + } + + override suspend fun renameTo(name: String): Boolean { + if (path == ROOT_PATH && name != ROOT_PATH) return false + + val normalized = fs.normalize(name) + if (normalized.contains('/')) throw IllegalArgumentException("'/' is not allowed in file or directory names. Given: '$name'.") + + val info = getFileFolderInfo() ?: return false + if (!info.isOperable()) return false + return bot.network.sendAndExpect( + if (info.isFile) { + FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized) + } else { + FileManagement.RenameFolder(client, contact.id, info.id, normalized) + } + ).toResult("RemoteFile.renameTo", checkResp = false).getOrThrow().int32RetCode == 0 + } + + /** + * null means not exist + */ + private suspend fun getIdSmart(): String? { + if (path == ROOT_PATH) return ROOT_PATH + return this.id ?: this.getFileFolderInfo()?.id + } + + override suspend fun moveTo(target: RemoteFile): Boolean { + if (target.checkIsImpl().contact != this.contact) { + // TODO: 2021/3/4 cross-group file move + + // target.mkdir() + // val targetFolderId = target.getIdSmart() ?: return false + // this.listFiles().mapNotNull { it.checkIsImpl().getFileFolderInfo() }.collect { + // FileManagement.MoveFile(client, contact.id, it.busId, it.id, it.parentFolderId, targetFolderId) + // .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow() + // + // // TODO: 2021/3/3 batch packets + // } + // this.delete() // it is now empty + + error("Cross-group file operation is not yet supported.") + } + if (target.path == this.path) return true + if (target.parent?.path == this.path) return false + val info = getFileFolderInfo() ?: return false + if (!info.isOperable()) return false + return if (info.isFile) { + val newParentId = target.parent?.checkIsImpl()?.getIdSmart() ?: return false + bot.network.sendAndExpect( + FileManagement.MoveFile( + client, + contact.id, + info.busId, + info.id, + info.parentFolderId, + newParentId + ) + ).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 + } else { + return bot.network.sendAndExpect(FileManagement.RenameFolder(client, contact.id, info.id, target.name)) + .toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 + } + } + + + override suspend fun mkdir(): Boolean { + if (path == ROOT_PATH) return false + if (!isBotOperator()) return false + + val parentFolderId: String = parent?.getIdSmart() ?: return false + + return bot.network.sendAndExpect(FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name)) + .toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0 + } + + private suspend fun upload0( + resource: ExternalResource, + callback: RemoteFile.ProgressionCallback?, + ): Oidb0x6d6.UploadFileRspBody? = resource.withAutoClose { + val parent = parent ?: return null + val parentInfo = parent.getFileFolderInfo() ?: return null + val resp = bot.network.sendAndExpect( + FileManagement.RequestUpload( + client, + groupCode = contact.id, + folderId = parentInfo.id, + resource = resource, + filename = this.name + ) + ).toResult("RemoteFile.upload").getOrThrow() + if (resp.boolFileExist) { + return resp + } + + val ext = GroupFileUploadExt( + u1 = 100, + u2 = 1, + entry = GroupFileUploadEntry( + business = ExcitingBusiInfo( + busId = resp.busId, + senderUin = bot.id, + receiverUin = contact.groupCode, // TODO: 2021/3/1 code or uin? + groupCode = contact.groupCode, + ), + fileEntry = ExcitingFileEntry( + fileSize = resource.size, + md5 = resource.md5, + sha1 = resource.sha1, + fileId = resp.fileId.toByteArray(), + uploadKey = resp.checkKey, + ), + clientInfo = ExcitingClientInfo( + clientType = 2, + appId = client.protocol.id.toString(), + terminalType = 2, + clientVer = "9e9c09dc", + unknown = 4, + ), + fileNameInfo = ExcitingFileNameInfo(this.name), + host = ExcitingHostConfig( + hosts = listOf( + ExcitingHostInfo( + url = ExcitingUrlInfo( + unknown = 1, + host = resp.uploadIpLanV4.firstOrNull() + ?: resp.uploadIpLanV6.firstOrNull() + ?: resp.uploadIp, + ), + port = resp.uploadPort, + ), + ), + ), + ), + u3 = 0, + ).toByteArray(GroupFileUploadExt.serializer()) + + callback?.onBegin(this, resource) + + kotlin.runCatching { + Highway.uploadResourceBdh( + bot = bot, + resource = resource, + kind = ResourceKind.GROUP_FILE, + commandId = 71, + extendInfo = ext, + dataFlag = 0, + callback = if (callback == null) null else fun(it: Long) { + callback.onProgression(this, resource, it) + } + ) + }.fold( + onSuccess = { + callback?.onSuccess(this, resource) + }, + onFailure = { + callback?.onFailure(this, resource, it) + } + ) + + return resp + } + + private suspend fun uploadInternal( + resource: ExternalResource, + callback: RemoteFile.ProgressionCallback?, + ): FileMessage { + val resp = upload0(resource, callback) ?: error("Failed to upload file.") + return FileMessageImpl( + resp.fileId, resp.busId, name, resource.size, allowSend = true + ) + } + + @Deprecated( + "Use uploadAndSend instead.", + replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"), + level = DeprecationLevel.ERROR + ) + override suspend fun upload( + resource: ExternalResource, + callback: RemoteFile.ProgressionCallback?, + ): FileMessage { + val msg = uploadInternal(resource, callback) + contact.sendMessage(msg + MiraiInternalMessageFlag) + return msg + } + + // compiler bug + @Deprecated( + "Use uploadAndSend instead.", + replaceWith = ReplaceWith("this.uploadAndSend(resource)"), + level = DeprecationLevel.ERROR + ) + @Suppress("DEPRECATION_ERROR") + override suspend fun upload(resource: ExternalResource): FileMessage { + return upload(resource, null) + } + + override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> { + @Suppress("DEPRECATION") + return contact.sendMessage(uploadInternal(resource, null) + MiraiInternalMessageFlag) + } + + override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? { + val info = getFileFolderInfo() ?: return null + if (!info.isFile) return null + val resp = bot.network.sendAndExpect( + FileManagement.RequestDownload( + client, + groupCode = contact.id, + busId = info.busId, + fileId = info.id + ) + ).toResult("RemoteFile.getDownloadInfo").getOrThrow() + + return RemoteFile.DownloadInfo( + filename = name, + id = info.id, + path = path, + url = "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" + + info.id.toByteArray().toUHexString(""), + sha1 = info.sha, + md5 = info.md5 + ) + } + + override fun toString(): String = path + + override suspend fun toMessage(): FileMessage? { + val info = getFileFolderInfo() ?: return null + if (!info.isFile) return null + return FileMessageImpl(info.id, info.busId, name, info.size) + } +} internal actual class RemoteFileImpl actual constructor( contact: Group, diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt index 3df223055..2e146ffb7 100644 --- a/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt +++ b/mirai-core/src/jvmBaseMain/kotlin/utils/crypto/ECDH.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.utils.crypto import net.mamoe.mirai.utils.decodeBase64 +import net.mamoe.mirai.utils.hexToBytes import java.security.KeyFactory import java.security.KeyPair import java.security.PrivateKey @@ -36,8 +37,13 @@ internal actual class ECDHKeyPairImpl( } -internal actual val publicKeyForVerify: ECDHPublicKey by lazy { +internal val publicKeyForVerify: ECDHPublicKey by lazy { KeyFactory.getInstance("RSA") .generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64())) } +private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes() + +internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey { + return ECDH.constructPublicKey(signHead + this) +} diff --git a/mirai-core/src/commonTest/kotlin/network/component/ComponentKeyTest.kt b/mirai-core/src/jvmBaseTest/kotlin/network/component/ComponentKeyTest.kt similarity index 100% rename from mirai-core/src/commonTest/kotlin/network/component/ComponentKeyTest.kt rename to mirai-core/src/jvmBaseTest/kotlin/network/component/ComponentKeyTest.kt diff --git a/mirai-core/src/commonTest/kotlin/network/component/ConcurrentComponentStorageTest.kt b/mirai-core/src/jvmBaseTest/kotlin/network/component/ConcurrentComponentStorageToStringTest.kt similarity index 83% rename from mirai-core/src/commonTest/kotlin/network/component/ConcurrentComponentStorageTest.kt rename to mirai-core/src/jvmBaseTest/kotlin/network/component/ConcurrentComponentStorageToStringTest.kt index 681b1b9e4..5672a4cf5 100644 --- a/mirai-core/src/commonTest/kotlin/network/component/ConcurrentComponentStorageTest.kt +++ b/mirai-core/src/jvmBaseTest/kotlin/network/component/ConcurrentComponentStorageToStringTest.kt @@ -12,19 +12,20 @@ package net.mamoe.mirai.internal.network.component import kotlin.test.Test import kotlin.test.assertEquals -internal data class TestComponent2( - val value: Int -) { - companion object : ComponentKey<TestComponent2> -} - -internal data class TestComponent3( - val value: Int -) { - companion object : ComponentKey<TestComponent3> -} internal class ConcurrentComponentStorageTest : AbstractMutableComponentStorageTest() { + internal data class TestComponent2( + val value: Int + ) { + companion object : ComponentKey<TestComponent2> + } + + internal data class TestComponent3( + val value: Int + ) { + companion object : ComponentKey<TestComponent3> + } + override fun createStorage(): MutableComponentStorage = ConcurrentComponentStorage(showAllComponents = true) @Test diff --git a/mirai-core/src/commonTest/kotlin/utils/test/StructureToStringTransformerNewTest.kt b/mirai-core/src/jvmBaseTest/kotlin/utils/test/StructureToStringTransformerNewTest.kt similarity index 100% rename from mirai-core/src/commonTest/kotlin/utils/test/StructureToStringTransformerNewTest.kt rename to mirai-core/src/jvmBaseTest/kotlin/utils/test/StructureToStringTransformerNewTest.kt diff --git a/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt b/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt index d2ee43d65..cfa36b4b8 100644 --- a/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt +++ b/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt @@ -12,14 +12,16 @@ package net.mamoe.mirai.internal.utils.crypto import net.mamoe.mirai.utils.decodeBase64 import net.mamoe.mirai.utils.md5 import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.security.* +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.Security +import java.security.Signature import java.security.spec.ECGenParameterSpec import java.security.spec.X509EncodedKeySpec import javax.crypto.KeyAgreement internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual companion object { - private const val curveName = "prime256v1" // p-256 actual val isECDHAvailable: Boolean diff --git a/mirai-core/src/jvmTest/kotlin/netinternalkit/NetReplayHelper.kt b/mirai-core/src/jvmTest/kotlin/netinternalkit/NetReplayHelper.kt index 455bb859b..8dfbf51cb 100644 --- a/mirai-core/src/jvmTest/kotlin/netinternalkit/NetReplayHelper.kt +++ b/mirai-core/src/jvmTest/kotlin/netinternalkit/NetReplayHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.network.components.ServerList import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory +import net.mamoe.mirai.internal.network.handler.SocketAddress import net.mamoe.mirai.internal.network.handler.selector.KeepAliveNetworkHandlerSelector import net.mamoe.mirai.internal.network.handler.selector.SelectorNetworkHandler import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler @@ -31,7 +32,6 @@ import java.awt.Component import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.lang.invoke.MethodHandles -import java.net.SocketAddress import javax.swing.* import kotlin.reflect.KProperty import kotlin.reflect.full.declaredMembers @@ -76,7 +76,7 @@ private fun NetReplayHelperClass(): Class<*> { private fun attachNetReplayHelper(channel: Channel) { - channel.pipeline() + channel.pipeline() // TODO: 2022/6/2 will not work since "raw-packet-collector" has been removed .addBefore("raw-packet-collector", "raw-packet-dumper", newRawPacketDumper()) attachNetReplayWView(channel) @@ -280,5 +280,6 @@ fun Bot.attachNetReplayHelper() { fun main() { val bot = BotFactory.newBot(0, "") - bot.attachNetReplayHelper() + bot.attachNetReplayHelper() // + // TODO: 2022/6/2 will not work since "raw-packet-collector" has been removed, see net.mamoe.mirai.internal.netinternalkit.NetReplayHelper.attachNetReplayHelper(io.netty.channel.Channel) } diff --git a/mirai-core/src/linuxX64Main/kotlin/MiraiImpl.kt b/mirai-core/src/linuxX64Main/kotlin/MiraiImpl.kt new file mode 100644 index 000000000..16390e369 --- /dev/null +++ b/mirai-core/src/linuxX64Main/kotlin/MiraiImpl.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal + +import io.ktor.client.* +import io.ktor.client.engine.curl.* +import io.ktor.client.features.* + +internal actual fun createDefaultHttpClient(): HttpClient { + return HttpClient(Curl) { + install(HttpTimeout) { + this.requestTimeoutMillis = 30_0000 + this.connectTimeoutMillis = 30_0000 + this.socketTimeoutMillis = 30_0000 + } + } +} diff --git a/mirai-core/src/linuxX64Main/kotlin/package.kt b/mirai-core/src/linuxX64Main/kotlin/package.kt new file mode 100644 index 000000000..7df5cebc4 --- /dev/null +++ b/mirai-core/src/linuxX64Main/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal \ No newline at end of file diff --git a/mirai-core/src/linuxX64Test/kotlin/package.kt b/mirai-core/src/linuxX64Test/kotlin/package.kt new file mode 100644 index 000000000..7df5cebc4 --- /dev/null +++ b/mirai-core/src/linuxX64Test/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal \ No newline at end of file diff --git a/mirai-core/src/mingwX64Main/cinterop/Socket.def b/mirai-core/src/mingwX64Main/cinterop/Socket.def new file mode 100644 index 000000000..eadcc2599 --- /dev/null +++ b/mirai-core/src/mingwX64Main/cinterop/Socket.def @@ -0,0 +1,31 @@ +headers = winsock.h + +--- + +#include <stdlib.h> +#include <string.h> + +#include <winsock.h> + +static int socket_create_connect(char *host, unsigned short port) { + struct hostent *he; + struct sockaddr_in their_addr; /* connector's address information */ + if ((he = gethostbyname(host)) == NULL) { /* get the host info */ + return -1; + } + int sockfd; + if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + return -2; + } + + their_addr.sin_family = AF_INET; /* host byte order */ + their_addr.sin_port = htons(port); /* short, network byte order */ + their_addr.sin_addr = *((struct in_addr *) he->h_addr); + bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */ + + if (connect(sockfd, (struct sockaddr *) &their_addr, sizeof(struct sockaddr)) == -1) { + return -3; + } + + return sockfd; +} \ No newline at end of file diff --git a/mirai-core/src/mingwX64Main/kotlin/MiraiImpl.kt b/mirai-core/src/mingwX64Main/kotlin/MiraiImpl.kt new file mode 100644 index 000000000..16390e369 --- /dev/null +++ b/mirai-core/src/mingwX64Main/kotlin/MiraiImpl.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal + +import io.ktor.client.* +import io.ktor.client.engine.curl.* +import io.ktor.client.features.* + +internal actual fun createDefaultHttpClient(): HttpClient { + return HttpClient(Curl) { + install(HttpTimeout) { + this.requestTimeoutMillis = 30_0000 + this.connectTimeoutMillis = 30_0000 + this.socketTimeoutMillis = 30_0000 + } + } +} diff --git a/mirai-core/src/mingwX64Main/kotlin/package.kt b/mirai-core/src/mingwX64Main/kotlin/package.kt new file mode 100644 index 000000000..7df5cebc4 --- /dev/null +++ b/mirai-core/src/mingwX64Main/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal \ No newline at end of file diff --git a/mirai-core/src/mingwX64Main/kotlin/utils/PlatformSocket.kt b/mirai-core/src/mingwX64Main/kotlin/utils/PlatformSocket.kt new file mode 100644 index 000000000..58542abdb --- /dev/null +++ b/mirai-core/src/mingwX64Main/kotlin/utils/PlatformSocket.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.errors.* +import kotlinx.cinterop.* +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel +import net.mamoe.mirai.utils.DEFAULT_BUFFER_SIZE +import net.mamoe.mirai.utils.toReadPacket +import net.mamoe.mirai.utils.wrapIO +import platform.posix.close +import platform.posix.read +import platform.posix.write +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * TCP Socket. + */ +internal actual class PlatformSocket( + private val socket: Int +) : Closeable, HighwayProtocolChannel { + @OptIn(ExperimentalCoroutinesApi::class) + private val dispatcher: CoroutineDispatcher = newSingleThreadContext("PlatformSocket#$socket.dispatcher") + + private val readLock = Mutex() + private val readBuffer = ByteArray(DEFAULT_BUFFER_SIZE).pin() + private val writeLock = Mutex() + private val writeBuffer = ByteArray(DEFAULT_BUFFER_SIZE).pin() + + actual val isOpen: Boolean + get() = write(socket, null, 0) != 0 + + @OptIn(ExperimentalIoApi::class) + actual override fun close() { + if (close(socket) != 0) { + throw PosixException.forErrno(posixFunctionName = "close()").wrapIO() + } + } + + @OptIn(ExperimentalIoApi::class) + actual suspend fun send(packet: ByteArray, offset: Int, length: Int): Unit = readLock.withLock { + withContext(dispatcher) { + require(offset >= 0) { "offset must >= 0" } + require(length >= 0) { "length must >= 0" } + require(offset + length <= packet.size) { "It must follows offset + length <= packet.size" } + packet.usePinned { pin -> + if (write(socket, pin.addressOf(offset), length.convert()) != 0) { + throw PosixException.forErrno(posixFunctionName = "close()").wrapIO() + } + } + } + } + + /** + * @throws SendPacketInternalException + */ + @OptIn(ExperimentalIoApi::class) + actual override suspend fun send(packet: ByteReadPacket): Unit = readLock.withLock { + withContext(dispatcher) { + val writeBuffer = writeBuffer + val length = packet.readAvailable(writeBuffer.get()) + if (write(socket, writeBuffer.addressOf(0), length.convert()) != 0) { + throw PosixException.forErrno(posixFunctionName = "close()").wrapIO() + } + } + } + + /** + * @throws ReadPacketInternalException + */ + actual override suspend fun read(): ByteReadPacket = writeLock.withLock { + withContext(dispatcher) { + val readBuffer = readBuffer + val length = read(socket, readBuffer.addressOf(0), readBuffer.get().size.convert()) + readBuffer.get().toReadPacket(length = length) + } + } + + actual companion object { + + @OptIn(UnsafeNumber::class, ExperimentalIoApi::class) + actual suspend fun connect( + serverIp: String, + serverPort: Int + ): PlatformSocket { + val r = sockets.socket_create_connect(serverIp.cstr, serverPort.toUShort()) + if (r < 0) error("Failed socket_create_connect: $r") + return PlatformSocket(r) + +// val addr = memScoped { +// alloc<sockaddr_in>() { +// sin_family = AF_INET.convert() +// sin_port = htons(serverPort.toUShort()) +// sin_addr.S_un +// sin_addr = resolveIpFromHost(serverIp).reinterpret<in_addr>().rawValue +// } +// }.reinterpret<sockaddr>() +// +// val id = socket(AF_INET, SOCK_STREAM, 0) +// if (id.toInt() == -1) throw PosixException.forErrno(posixFunctionName = "socket()") +// +// val conn = connect(id, addr.ptr, sizeOf<sockaddr_in>().convert()) +// if (conn != 0) throw PosixException.forErrno(posixFunctionName = "connect()") +// +// return PlatformSocket(conn) + } + +// private fun resolveIpFromHost(serverIp: String): CPointer<hostent> { +// return gethostbyname(serverIp) +// ?: throw IllegalStateException("Failed to resolve IP from host. host=$serverIp") +// } + + actual suspend inline fun <R> withConnection( + serverIp: String, + serverPort: Int, + block: PlatformSocket.() -> R + ): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return connect(serverIp, serverPort).use(block) + } + } + +} diff --git a/mirai-core/src/nativeMain/cinterop/OpenSSL.def b/mirai-core/src/nativeMain/cinterop/OpenSSL.def new file mode 100644 index 000000000..f4e064188 --- /dev/null +++ b/mirai-core/src/nativeMain/cinterop/OpenSSL.def @@ -0,0 +1,43 @@ +headers = openssl/ec.h openssl/ecdh.h openssl/evp.h + +linkerOpts.osx = -lcrypto \ + -lssl \ + -L/opt/openssl/lib \ + -L/opt/homebrew/Cellar/openssl@3/3.0.3/lib \ + -L/opt/homebrew/opt/openssl@3/lib \ + +compilerOpts.osx = -I/opt/openssl/include \ + -I/usr/local/include/openssl@3 \ + -I/opt/homebrew/Cellar/openssl@3/3.0.3/include \ + -I/usr/include/openssl@3 \ + -I/opt/homebrew/opt/openssl@3/include + +linkerOpts.linux = -lcrypto \ + -lssl \ + -L/usr/lib64 \ + -L/usr/lib/x86_64-linux-gnu \ + -L/opt/local/lib \ + -L/usr/local/opt/openssl@3/lib \ + -L/opt/homebrew/opt/openssl@3/lib + +compilerOpts.linux = -I/opt/local/include/openssl@3 \ + -I/usr/bin/openssl@3 \ + -I/usr/local/include/openssl@3 \ + -I/usr/include/openssl@3 \ + -I/opt/homebrew/opt/openssl@3/include + +linkerOpts.mingw_x64 = -lcrypto \ + -lssl \ + -L/usr/lib64 \ + -L/usr/lib/x86_64-linux-gnu \ + -L/opt/local/lib \ + -L/usr/local/opt/openssl@3/lib \ + -L/opt/homebrew/opt/openssl@3/lib \ + -LC:/Tools/msys64/mingw64/lib \ + -LC:/Tools/msys2/mingw64/lib + +compilerOpts.mingw_x64 = -I/opt/local/include/openssl@3 \ + -I/usr/bin/openssl@3 \ + -I/usr/local/include/openssl@3 \ + -I/usr/include/openssl@3 \ + -I/opt/homebrew/opt/openssl@3/include diff --git a/mirai-core/src/nativeMain/kotlin/MiraiImpl.kt b/mirai-core/src/nativeMain/kotlin/MiraiImpl.kt new file mode 100644 index 000000000..52f44d2e0 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/MiraiImpl.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal + +import kotlinx.atomicfu.atomic +import net.mamoe.mirai.internal.utils.MiraiCoreServices + + +public fun initMirai() { + _MiraiImpl_static_init() +} + + +private val initialized = atomic(false) + +@Suppress("FunctionName") +internal actual fun _MiraiImpl_static_init() { + if (!initialized.compareAndSet(expect = false, update = true)) return + MiraiCoreServices.registerAll() +} diff --git a/mirai-core/src/nativeMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/nativeMain/kotlin/contact/GroupImpl.kt new file mode 100644 index 000000000..f73074a7f --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/contact/GroupImpl.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.contact + +import net.mamoe.mirai.contact.ContactList +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.data.GroupInfo +import net.mamoe.mirai.internal.QQAndroidBot +import kotlin.coroutines.CoroutineContext + +internal actual class GroupImpl actual constructor( + bot: QQAndroidBot, + parentCoroutineContext: CoroutineContext, + id: Long, + groupInfo: GroupInfo, + members: ContactList<NormalMemberImpl>, +) : Group, CommonGroupImpl(bot, parentCoroutineContext, id, groupInfo, members) { + actual companion object; +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/network/handler/LengthDelimitedPacketReader.kt b/mirai-core/src/nativeMain/kotlin/network/handler/LengthDelimitedPacketReader.kt new file mode 100644 index 000000000..ca1fa3ba3 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/network/handler/LengthDelimitedPacketReader.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.handler + +import io.ktor.utils.io.core.* +import net.mamoe.mirai.utils.* + +private val debugLogger: MiraiLogger by lazy { + MiraiLogger.Factory.create( + LengthDelimitedPacketReader::class, "LengthDelimitedPacketReader" + ).withSwitch(systemProp("mirai.network.handler.length.delimited.packet.reader.debug", false)) +} + +/** + * Not thread-safe + */ +internal class LengthDelimitedPacketReader( + private val sendDecode: (combined: ByteReadPacket) -> Unit +) : Closeable { + private var missingLength: Long = 0 + set(value) { + field = value + debugLogger.info { "missingLength = $field" } + } + private val bufferedParts: MutableList<ByteReadPacket> = ArrayList(10) + + @TestOnly + fun getMissingLength() = missingLength + + @TestOnly + fun getBufferedPackets() = bufferedParts.toList() + + fun offer(packet: ByteReadPacket) { + if (missingLength == 0L) { + // initial + debugLogger.info { "initial length == 0" } + missingLength = packet.readInt().toLongUnsigned() - 4 + } + debugLogger.info { "Offering packet len = ${packet.remaining}" } + missingLength -= packet.remaining + bufferedParts.add(packet) + if (missingLength <= 0) { + emit() + } + } + + private fun emit() { + debugLogger.info { "Emitting, buffered = ${bufferedParts.map { it.remaining }}" } + when (bufferedParts.size) { + 0 -> {} + 1 -> { + val part = bufferedParts.first() + if (missingLength == 0L) { + debugLogger.info { "Single packet length perfectly matched." } + sendDecode(part) + + bufferedParts.clear() + } else { + check(missingLength < 0L) { "Failed check: remainingLength < 0L" } + + val previousPacketLength = missingLength + part.remaining + debugLogger.info { "Got extra packets, previousPacketLength = $previousPacketLength" } + sendDecode(part.readPacketExact(previousPacketLength.toInt())) + + bufferedParts.clear() + + // now packet contain new part. + missingLength = part.readInt().toLongUnsigned() - 4 + offer(part) + } + } + else -> { + if (missingLength == 0L) { + debugLogger.info { "Multiple packets length perfectly matched." } + sendDecode(buildPacket(bufferedParts.sumOf { it.remaining }.toInt()) { + bufferedParts.forEach { writePacket(it) } + }) + + bufferedParts.clear() + } else { + val lastPart = bufferedParts.last() + val previousPacketPartLength = missingLength + lastPart.remaining + debugLogger.debug { "previousPacketPartLength = $previousPacketPartLength" } + val combinedLength = + (bufferedParts.sumOf { it.remaining } - lastPart.remaining // buffered length without last part + + previousPacketPartLength).toInt() + debugLogger.debug { "combinedLength = $combinedLength" } + + if (combinedLength < 0) return // not enough, still more parts missing. + + sendDecode(buildPacket(combinedLength) { + repeat(bufferedParts.size - 1) { i -> + writePacket(bufferedParts[i]) + } + writePacket(lastPart, previousPacketPartLength) + }) + + bufferedParts.clear() + + // now packet contain new part. + missingLength = lastPart.readInt().toLongUnsigned() - 4 + offer(lastPart) + } + + } + } + } + + override fun close() { + bufferedParts.forEach { it.close() } + } +} diff --git a/mirai-core/src/nativeMain/kotlin/network/handler/NativeNetworkHandler.kt b/mirai-core/src/nativeMain/kotlin/network/handler/NativeNetworkHandler.kt new file mode 100644 index 000000000..599d40711 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/network/handler/NativeNetworkHandler.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.handler + +import io.ktor.utils.io.* +import io.ktor.utils.io.core.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import net.mamoe.mirai.internal.network.components.PacketCodec +import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.internal.utils.PlatformSocket +import net.mamoe.mirai.internal.utils.connect +import net.mamoe.mirai.utils.childScope +import net.mamoe.mirai.utils.info + +internal class NativeNetworkHandler( + context: NetworkHandlerContext, + address: SocketAddress +) : CommonNetworkHandler<NativeNetworkHandler.NativeConn>(context, address) { + internal object Factory : NetworkHandlerFactory<NativeNetworkHandler> { + override fun create(context: NetworkHandlerContext, address: SocketAddress): NativeNetworkHandler { + return NativeNetworkHandler(context, address) + } + } + + internal inner class NativeConn( + private val socket: PlatformSocket, + ) : Closeable, CoroutineScope by coroutineContext.childScope("NativeConn") { + private val decodePipeline: PacketDecodePipeline = PacketDecodePipeline(this.coroutineContext) + + private val packetCodec: PacketCodec by lazy { context[PacketCodec] } + private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] } + + private val sendQueue: Channel<OutgoingPacket> = Channel(Channel.BUFFERED) { undelivered -> + launch { write(undelivered) } + } + + private val lengthDelimitedPacketReader = LengthDelimitedPacketReader(decodePipeline::send) + + init { + launch { + while (isActive) { + val result = sendQueue.receiveCatching() + logger.info { "Native sender: $result" } + result.onFailure { if (it is CancellationException) return@launch } + + result.getOrNull()?.let { packet -> + try { + socket.send(packet.delegate, 0, packet.delegate.size) + } catch (e: Throwable) { + if (e is CancellationException) return@launch + logger.error("Error while sending packet '${packet.commandName}'", e) + } + } + } + } + + launch { + while (isActive) { + try { + val packet = socket.read() + + lengthDelimitedPacketReader.offer(packet) + } catch (e: Throwable) { + if (e is CancellationException) return@launch + logger.error("Error while reading packet.", e) + setState { StateClosed(e) } + } + } + } + } + + fun write(packet: OutgoingPacket) { + sendQueue.trySend(packet).onFailure { + throw it + ?: throw IllegalStateException("Internal error: Failed to send packet '${packet.commandName}' without reason.") + } + } + + override fun close() { + cancel() + sendQueue.close() + } + } + + override suspend fun createConnection(): NativeConn { + logger.info { "Connecting to $address" } + return NativeConn(PlatformSocket.connect(address)).also { + logger.info { "Connected to server $address" } + } + } + + @Suppress("EXTENSION_SHADOWED_BY_MEMBER") + override fun NativeConn.close() { + this.close() + } + + override fun NativeConn.writeAndFlushOrCloseAsync(packet: OutgoingPacket) { + write(packet) + } +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/network/handler/NetworkHandlerFactory.kt b/mirai-core/src/nativeMain/kotlin/network/handler/NetworkHandlerFactory.kt new file mode 100644 index 000000000..4eb675111 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/network/handler/NetworkHandlerFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.handler + +/** + * Factory for a specific [NetworkHandler] implementation. + */ +internal actual fun interface NetworkHandlerFactory<out H : NetworkHandler> { + actual fun create( + context: NetworkHandlerContext, + host: String, + port: Int + ): H = create(context, SocketAddressImpl(host, port)) + + /** + * Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED] + */ + actual fun create( + context: NetworkHandlerContext, + address: SocketAddress + ): H + + actual companion object { + actual fun getPlatformDefault(): NetworkHandlerFactory<*> = NativeNetworkHandler.Factory + } +} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/network/handler/SocketAddress.kt b/mirai-core/src/nativeMain/kotlin/network/handler/SocketAddress.kt index 44fb18548..2baca8668 100644 --- a/mirai-core/src/nativeMain/kotlin/network/handler/SocketAddress.kt +++ b/mirai-core/src/nativeMain/kotlin/network/handler/SocketAddress.kt @@ -9,41 +9,23 @@ package net.mamoe.mirai.internal.network.handler -import net.mamoe.mirai.internal.network.handler.NetworkHandler.State - internal actual abstract class SocketAddress( - actual val host: String, - actual val port: Int, + val host: String, + val port: Int, @Suppress("UNUSED_PARAMETER") constructorMarker: Unit?, // avoid ambiguity with function SocketAddress ) -internal class SocketAddressImpl(host: String, port: Int) : SocketAddress(host, port, null) +internal actual fun SocketAddress.getHost(): String = host +internal actual fun SocketAddress.getPort(): Int = port -internal actual fun SocketAddress(host: String, port: Int): SocketAddress { + +internal class SocketAddressImpl(host: String, port: Int) : SocketAddress(host, port, null) { + override fun toString(): String { + return "$host:$port" + } +} + +internal actual fun createSocketAddress(host: String, port: Int): SocketAddress { return SocketAddressImpl(host, port) } -/** - * Factory for a specific [NetworkHandler] implementation. - */ -internal actual fun interface NetworkHandlerFactory<out H : NetworkHandler> { - actual fun create( - context: NetworkHandlerContext, - host: String, - port: Int - ): H = create(context, SocketAddressImpl(host, port)) - - /** - * Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED] - */ - actual fun create( - context: NetworkHandlerContext, - address: SocketAddress - ): H - - actual companion object { - actual fun getPlatformDefault(): NetworkHandlerFactory<*> { - TODO("Not yet implemented") - } - } -} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/utils/MiraiCoreServices.kt b/mirai-core/src/nativeMain/kotlin/utils/MiraiCoreServices.kt index 350c98d13..0535107a9 100644 --- a/mirai-core/src/nativeMain/kotlin/utils/MiraiCoreServices.kt +++ b/mirai-core/src/nativeMain/kotlin/utils/MiraiCoreServices.kt @@ -28,6 +28,10 @@ internal object MiraiCoreServices { val msgProtocol = "net.mamoe.mirai.internal.message.protocol.MessageProtocol" + Services.register( + msgProtocol, + "net.mamoe.mirai.internal.message.protocol.impl.AudioProtocol" + ) { net.mamoe.mirai.internal.message.protocol.impl.AudioProtocol() } Services.register( msgProtocol, "net.mamoe.mirai.internal.message.protocol.impl.CustomMessageProtocol" diff --git a/mirai-core/src/nativeMain/kotlin/utils/PlatformSocket.kt b/mirai-core/src/nativeMain/kotlin/utils/PlatformSocket.kt index e60e08d15..e32e0fbb9 100644 --- a/mirai-core/src/nativeMain/kotlin/utils/PlatformSocket.kt +++ b/mirai-core/src/nativeMain/kotlin/utils/PlatformSocket.kt @@ -9,58 +9,7 @@ package net.mamoe.mirai.internal.utils -import io.ktor.utils.io.core.* import io.ktor.utils.io.errors.* -import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel - -/** - * TCP Socket. - */ -internal actual class PlatformSocket : Closeable, HighwayProtocolChannel { - actual val isOpen: Boolean - get() = TODO("Not yet implemented") - - actual override fun close() { - } - - actual suspend fun send(packet: ByteArray, offset: Int, length: Int) { - } - - /** - * @throws SendPacketInternalException - */ - actual override suspend fun send(packet: ByteReadPacket) { - } - - /** - * @throws ReadPacketInternalException - */ - actual override suspend fun read(): ByteReadPacket { - TODO("Not yet implemented") - } - - actual suspend fun connect(serverHost: String, serverPort: Int) { - } - - actual companion object { - actual suspend fun connect( - serverIp: String, - serverPort: Int - ): PlatformSocket { - TODO("Not yet implemented") - } - - actual suspend inline fun <R> withConnection( - serverIp: String, - serverPort: Int, - block: PlatformSocket.() -> R - ): R { - TODO("Not yet implemented") - } - - } - -} internal actual class SocketException : IOException { actual constructor() : super("", null) @@ -76,4 +25,4 @@ internal actual class NoRouteToHostException : IOException { internal actual class UnknownHostException : IOException { actual constructor() : super("") actual constructor(message: String) : super(message) -} \ No newline at end of file +} diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt new file mode 100644 index 000000000..064820081 --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDH.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.crypto + +import kotlinx.cinterop.* +import net.mamoe.mirai.utils.hexToBytes +import net.mamoe.mirai.utils.md5 +import net.mamoe.mirai.utils.toUHexString +import openssl.* +import platform.posix.errno +import platform.posix.free + +private const val curveId = NID_X9_62_prime256v1 + +// shared, not freed! +private val group by lazy { EC_GROUP_new_by_curve_name(curveId) ?: error("Failed to get EC_GROUP") } + +private val convForm by lazy { EC_GROUP_get_point_conversion_form(group) } + +// shared, not freed! +private val bnCtx by lazy { BN_CTX_new() } + + +internal actual interface ECDHPublicKey : OpenSSLKey { + val encoded: ByteArray + fun toPoint(): CPointer<EC_POINT> +} + +internal actual interface ECDHPrivateKey : OpenSSLKey { + fun toBignum(): CPointer<BIGNUM> +} + +internal class OpenSslPrivateKey( + override val hex: String, // use Kotlin's memory +) : ECDHPrivateKey { + + override fun toBignum(): CPointer<BIGNUM> { + val bn = BN_new() ?: error("Failed BN_new") + val values = cValuesOf(bn) + BN_hex2bn(values, hex).let { r -> + if (r <= 0) error("Failed BN_hex2bn: $r") + } + return bn + } + + companion object { + fun fromKey(key: CPointer<EC_KEY>): OpenSslPrivateKey { + val bn = EC_KEY_get0_private_key(key) ?: error("Failed EC_KEY_get0_private_key") + val hex = try { + val ptr = BN_bn2hex(bn) ?: error("Failed EC_POINT_bn2point") + try { + ptr.toKString() + } finally { + free(ptr) + } + } finally { + BN_free(bn) + } + return OpenSslPrivateKey(hex) + } + } +} + +internal interface OpenSSLKey { + val hex: String +} + +internal class OpenSslPublicKey(override val hex: String) : ECDHPublicKey { + override val encoded: ByteArray = hex.hexToBytes() + + override fun toPoint(): CPointer<EC_POINT> { + val point = EC_POINT_new(group) + EC_POINT_hex2point(group, hex, point, bnCtx) ?: error("Failed EC_POINT_hex2point") + return point!! + } + + companion object { + fun fromKey(key: CPointer<EC_KEY>): OpenSslPublicKey = + fromPoint(EC_KEY_get0_public_key(key) ?: error("Failed to get private key")) + + fun fromPoint(point: CPointer<EC_POINT>): OpenSslPublicKey { + return OpenSslPublicKey(point.toKtHex()) + } + } +} + +internal actual class ECDHKeyPairImpl( + override val privateKey: OpenSslPrivateKey, + override val publicKey: OpenSslPublicKey, + initialPublicKey: ECDHPublicKey +) : ECDHKeyPair { + + override val maskedPublicKey: ByteArray by lazy { publicKey.encoded } + override val maskedShareKey: ByteArray by lazy { ECDH.calculateShareKey(privateKey, initialPublicKey) } + + companion object { + fun fromKey( + key: CPointer<EC_KEY>, + initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key + ): ECDHKeyPairImpl { + return ECDHKeyPairImpl(OpenSslPrivateKey.fromKey(key), OpenSslPublicKey.fromKey(key), initialPublicKey) + } + } +} + +private fun CPointer<EC_POINT>.toKtHex(): String { + val ptr = EC_POINT_point2hex(group, this, convForm, bnCtx) ?: error("Failed EC_POINT_point2hex") + return try { + ptr.toKString() + } finally { + free(ptr) + } +} + + +internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { + + /** + * 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey + */ + actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray { + return calculateShareKey(keyPair.privateKey, peerPublicKey) + } + + actual companion object { + actual val isECDHAvailable: Boolean get() = true + + /** + * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey] + */ + actual fun constructPublicKey(key: ByteArray): ECDHPublicKey { + val p = EC_POINT_new(group) ?: error("Failed to create EC_POINT") + + // TODO: 2022/6/1 native: check memory + EC_POINT_hex2point(group, key.toUHexString("").lowercase(), p, bnCtx) + + return OpenSslPublicKey.fromPoint(p) + } + + /** + * 由完整的 rsaKey 校验 publicKey + */ + actual fun verifyPublicKey( + version: Int, + publicKey: String, + publicKeySign: String + ): Boolean = true + + /** + * 生成随机密匙对 + */ + actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair { + val key: CPointer<EC_KEY> = EC_KEY_new_by_curve_name(curveId) + ?: throw IllegalStateException("Failed to create key curve, $errno") + + if (1 != EC_KEY_generate_key(key)) { + throw IllegalStateException("Failed to generate key, $errno") + } + + try { + return ECDHKeyPairImpl.fromKey(key, initialPublicKey) + } finally { + free(key) // TODO: THIS MAY CAUSE MEMORY LEAK. But EC_KEY_free() will terminate the process for unknown reason. + } + } + + fun calculateCanonicalShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray { + check(publicKey is OpenSslPublicKey) + check(privateKey is OpenSslPrivateKey) + + val k = EC_KEY_new_by_curve_name(curveId) ?: error("Failed to create EC key") + try { + val privateBignum = privateKey.toBignum() + try { + EC_KEY_set_private_key(k, privateBignum).let { r -> + if (r != 1) error("Failed EC_KEY_set_private_key: $r") + } + + val fieldSize = EC_GROUP_get_degree(group) + if (fieldSize <= 0) { + error("Failed EC_GROUP_get_degree: $fieldSize") + } + + var secretLen = (fieldSize + 7) / 8 + + val publicPoint = publicKey.toPoint() + try { + ByteArray(secretLen.convert()).usePinned { pin -> + secretLen = ECDH_compute_key(pin.addressOf(0), secretLen.convert(), publicPoint, k, null) + if (secretLen <= 0) { + error("Failed to compute secret") + } + + return pin.get().copyOf(secretLen) + } + } finally { + EC_POINT_free(publicPoint) + } + } finally { + BN_free(privateBignum) + } + } finally { + EC_KEY_free(k) + } + } + + actual fun calculateShareKey( + privateKey: ECDHPrivateKey, + publicKey: ECDHPublicKey + ): ByteArray = calculateCanonicalShareKey(privateKey, publicKey).copyOf(16).md5() + } + + actual override fun toString(): String = "ECDH($keyPair)" +} + +internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey { + return ECDH.constructPublicKey(this) +} diff --git a/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDHPrivateKey.kt b/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDHPrivateKey.kt deleted file mode 100644 index 36785f9d9..000000000 --- a/mirai-core/src/nativeMain/kotlin/utils/crypto/ECDHPrivateKey.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -package net.mamoe.mirai.internal.utils.crypto - -internal actual interface ECDHPrivateKey -internal actual interface ECDHPublicKey { - actual fun getEncoded(): ByteArray -} - -internal actual class ECDHKeyPairImpl( - override val privateKey: ECDHPrivateKey, - override val publicKey: ECDHPublicKey, - override val maskedShareKey: ByteArray, - override val maskedPublicKey: ByteArray -) : ECDHKeyPair - -/** - * 椭圆曲线密码, ECDH 加密 - */ -internal actual class ECDH actual constructor(keyPair: ECDHKeyPair) { - actual val keyPair: ECDHKeyPair - get() = TODO("Not yet implemented") - - /** - * 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey - */ - actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray { - TODO("Not yet implemented") - } - - actual companion object { - actual val isECDHAvailable: Boolean - get() = TODO("Not yet implemented") - - /** - * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey] - */ - actual fun constructPublicKey(key: ByteArray): ECDHPublicKey { - TODO("Not yet implemented") - } - - /** - * 由完整的 rsaKey 校验 publicKey - */ - actual fun verifyPublicKey( - version: Int, - publicKey: String, - publicKeySign: String - ): Boolean { - TODO("Not yet implemented") - } - - /** - * 生成随机密匙对 - */ - actual fun generateKeyPair(initialPublicKey: ECDHPublicKey): ECDHKeyPair { - TODO("Not yet implemented") - } - - /** - * 由一对密匙计算 shareKey - */ - actual fun calculateShareKey( - privateKey: ECDHPrivateKey, - publicKey: ECDHPublicKey - ): ByteArray { - TODO("Not yet implemented") - } - - } - - actual override fun toString(): String { - TODO("Not yet implemented") - } - -} - -internal actual val publicKeyForVerify: ECDHPublicKey - get() = TODO("Not yet implemented") diff --git a/mirai-core/src/nativeTest/kotlin/network/LengthDelimitedPacketReaderTest.kt b/mirai-core/src/nativeTest/kotlin/network/LengthDelimitedPacketReaderTest.kt new file mode 100644 index 000000000..fc3fe2bb9 --- /dev/null +++ b/mirai-core/src/nativeTest/kotlin/network/LengthDelimitedPacketReaderTest.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +@file:OptIn(TestOnly::class) + +package net.mamoe.mirai.internal.network + +import io.ktor.utils.io.core.* +import net.mamoe.mirai.internal.network.handler.LengthDelimitedPacketReader +import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.internal.utils.io.writeIntLVPacket +import net.mamoe.mirai.internal.utils.io.writeShortLVString +import net.mamoe.mirai.utils.* +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class LengthDelimitedPacketReaderTest : AbstractTest() { + init { + setSystemProp("mirai.network.handler.length.delimited.packet.reader.debug", "true") + } + + private val received = mutableListOf<ByteArray>() + private val reader = LengthDelimitedPacketReader { received.add(it.readBytes()) } + + /* + * All these tests cases can happen in the real time, and even before logon is complete. + */ + + @Test + fun `can read exact packet`() { + val original = buildLVPacket { + writeShortLVString("some strings") + writeInt(123) + } + val originalLength = original.remaining + reader.offer(original) + + assertEquals(1, received.size) + received.single().read { + assertEquals(originalLength - 4, this.remaining) + assertEquals("some strings", readUShortLVString()) + assertEquals(123, readInt()) + assertEquals(0, remaining) + } + + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + } + + @Test + fun `can read 2 part packets`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(123) + }.readBytes() + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(123) + }.readBytes() + + reader.offer(buildPacket { + writeInt(part1.size + part2.size + 4) + writeFully(part1) + }) + assertEquals(0, received.size) + + reader.offer(part2.toReadPacket()) + assertEquals(1, received.size) + + received.single().read { + assertEquals(part1.size + part2.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(123, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(123, readInt()) + assertEquals(0, remaining) + } + + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + } + + @Test + fun `can read 3 part packets`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(111) + }.readBytes() + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(222) + }.readBytes() + + val part3 = buildPacket { + writeShortLVString("some strings") + writeInt(333) + }.readBytes() + + reader.offer(buildPacket { + writeInt(part1.size + part2.size + part3.size + 4) + writeFully(part1) + + // part2 and part3 missing + }) + assertEquals(0, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(part2.size + part3.size, reader.getMissingLength().toInt()) + + reader.offer(part2.toReadPacket()) + + assertEquals(0, received.size) + assertEquals(2, reader.getBufferedPackets().size) + assertEquals(part3.size, reader.getMissingLength().toInt()) + + reader.offer(part3.toReadPacket()) + + assertEquals(1, received.size) + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + + received.single().read { + assertEquals(part1.size + part2.size + part3.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(111, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(222, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(333, readInt()) + assertEquals(0, remaining) + } + } + + @Test + fun `can read 3 part packets with a combined`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(111) + }.readBytes() + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(222) + }.readBytes() + + val part3 = buildPacket { + writeShortLVString("some strings") + writeInt(333) + }.readBytes() + + val part4 = buildPacket { + writeShortLVString("some strings") + writeInt(444) + }.readBytes() + + reader.offer(buildPacket { + writeInt(part1.size + part2.size + part3.size + 4) + writeFully(part1) + + // part2 and part3 missing + }) + assertEquals(0, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(part2.size + part3.size, reader.getMissingLength().toInt()) + + reader.offer(part2.toReadPacket()) + + assertEquals(0, received.size) + assertEquals(2, reader.getBufferedPackets().size) + assertEquals(part3.size, reader.getMissingLength().toInt()) + + reader.offer(buildPacket { + writeFully(part3) + writePacket(buildLVPacket { writeFully(part4) }) + }) + + assertEquals(2, received.size) + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + + received[0].read { + assertEquals(part1.size + part2.size + part3.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(111, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(222, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(333, readInt()) + assertEquals(0, remaining) + } + + received[1].read { + assertEquals(part4.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(444, readInt()) + assertEquals(0, remaining) + } + } + + @Test + fun `can read 3 part packets from combined with a combined`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(111) + }.readBytes() + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(222) + }.readBytes() + + val part3 = buildPacket { + writeShortLVString("some strings") + writeInt(333) + }.readBytes() + + val part4 = buildPacket { + writeShortLVString("some strings") + writeInt(444) + }.readBytes() + + val part5 = buildPacket { + writeShortLVString("some strings") + writeInt(555) + }.readBytes() + + reader.offer(buildPacket { + writeInt(part1.size + part2.size + part3.size + 4) + writeFully(part1) + + // part2 and part3 missing + }) + + assertEquals(0, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(part2.size + part3.size, reader.getMissingLength().toInt()) + + reader.offer(part2.toReadPacket()) + + assertEquals(0, received.size) + assertEquals(2, reader.getBufferedPackets().size) + assertEquals(part3.size, reader.getMissingLength().toInt()) + + reader.offer(buildPacket { + writeFully(part3) + writePacket(buildPacket { + writeInt(part4.size + part5.size + 4) + writeFully(part4) + + // part5 missing + }) + }) + + assertEquals(1, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(part5.size, reader.getMissingLength().toInt()) + + reader.offer(part5.toReadPacket()) + + assertEquals(2, received.size) + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength().toInt()) + + received[0].read { + assertEquals(part1.size + part2.size + part3.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(111, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(222, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(333, readInt()) + assertEquals(0, remaining) + } + + received[1].read { + assertEquals(part4.size + part5.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(444, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(555, readInt()) + assertEquals(0, remaining) + } + } + + // Ensures it will not emit without any length check if received a missing part. + @Test + fun `can read 4 part packets`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(111) + }.readBytes() + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(222) + }.readBytes() + + val part3 = buildPacket { + writeShortLVString("some strings") + writeInt(333) + }.readBytes() + + val part4 = buildPacket { + writeShortLVString("some strings") + writeInt(444) + }.readBytes() + + reader.offer(buildPacket { + writeInt(part1.size + part2.size + part3.size + part4.size + 4) + writeFully(part1) + + // part2, part3 and part4 missing + }) + assertEquals(0, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(part2.size + part3.size + part4.size, reader.getMissingLength().toInt()) + + reader.offer(part2.toReadPacket()) + + assertEquals(0, received.size) + assertEquals(2, reader.getBufferedPackets().size) + assertEquals(part3.size + part4.size, reader.getMissingLength().toInt()) + + reader.offer(part3.toReadPacket()) + + assertEquals(0, received.size) + assertEquals(3, reader.getBufferedPackets().size) + assertEquals(part4.size, reader.getMissingLength().toInt()) + + reader.offer(part4.toReadPacket()) + + assertEquals(1, received.size) + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + + received.single().read { + assertEquals(part1.size + part2.size + part3.size + part4.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(111, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(222, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(333, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(444, readInt()) + assertEquals(0, remaining) + } + } + + + @Test + fun `can read 2 combined packets`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(123) + }.readBytes() + + println("part1.size = ${part1.size}") + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(222) + }.readBytes() + + println("part2.size = ${part2.size}") + + reader.offer(buildPacket { + writePacket(buildLVPacket { writeFully(part1) }) + writePacket(buildLVPacket { writeFully(part2) }) + }) + + assertEquals(2, received.size) + + received[0].read { + assertEquals(part1.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(123, readInt()) + assertEquals(0, remaining) + } + + received[1].read { + assertEquals(part2.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(222, readInt()) + assertEquals(0, remaining) + } + + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + } + + @Test + fun `can emit 2 combined packets with another part`() { + val part1 = buildPacket { + writeShortLVString("some strings") + writeInt(111) + }.readBytes() + + val part2 = buildPacket { + writeShortLVString("some strings") + writeInt(222) + }.readBytes() + + val part3 = buildPacket { + writeShortLVString("some strings") + writeInt(333) + }.readBytes() + + val part4 = buildPacket { + writeShortLVString("some strings") + writeInt(444) + }.readBytes() + + + println("part1.size = ${part1.size}") + println("part2.size = ${part2.size}") + println("part3.size = ${part3.size}") + println("part4.size = ${part4.size}") + + reader.offer(buildPacket { + // should emit two packets + writePacket(buildLVPacket { writeFully(part1) }) + writePacket(buildLVPacket { writeFully(part2) }) + + // and process this part + writePacket(buildPacket { + writeInt(part3.size + part4.size + 4) + writeFully(part3) + // part4 missing + }) + }) + + assertEquals(2, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(part4.size, reader.getMissingLength().toInt()) + + received[0].read { + assertEquals(part1.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(111, readInt()) + assertEquals(0, remaining) + } + + received[1].read { + assertEquals(part2.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(222, readInt()) + assertEquals(0, remaining) + } + + // part4, here you are + reader.offer(buildPacket { + writePacket(buildPacket { writeFully(part4) }) + }) + + received[2].read { + assertEquals(part3.size + part4.size, this.remaining.toInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(333, readInt()) + assertEquals("some strings", readUShortLVString()) + assertEquals(444, readInt()) + assertEquals(0, remaining) + } + + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength()) + } + + @Test + fun `real case test 1`() { + // 5696 + val part1 = + """00 00 02 F0 00 00 00 0B 01 00 00 00 00 0E 11 11 11 11 11 11 11 11 11 11 6D A0 E3 2B 65 53 C5 AE 22 D0 7B AC 3D DD 3C 6E FF 24 84 38 F0 B7 A3 DE 5F 3A 15 DE 7E 6D 08 D1 8C 18 A9 F2 89 03 03 43 15 C6 32 01 74 5B DD A4 33 49 47 78 E0 5B 83 2C E7 3A E1 CC 50 9C A8 8C 5C 88 06 A0 90 04 6E 23 6F AD 84 D8 6B 10 64 AD 33 5B 3F B5 3C C3 24 6C BB 28 8C BC A2 BD 5E 91 EA FA FE 5C 3B C3 F4 3B 59 24 37 1C E2 13 DB 75 C1 7C D5 8B 2F 57 AD C0 16 13 97 12 60 D5 4C 21 E8 13 72 B3 F6 98 05 89 BA 49 60 F1 1C D9 6A 82 F6 A1 AE 78 82 48 60 A9 24 3C A7 7A 93 79 96 8B AA E2 FA DE 5F E9 12 FE 51 27 47 CB 6A 20 DB 64 22 B4 3A B4 C8 5E D0 45 31 9F 63 9B 61 A4 F9 56 CB 0B BF 27 21 55 99 2A 53 EA E4 DC EA D3 7E 62 B9 1D 2A 48 54 C3 6B D8 E0 CD A5 CC 84 AF A8 91 05 2D AC C3 64 82 5F 56 EA FB 13 5B A7 78 ED B1 E1 ED 25 74 1D EE 5D A3 5F 2E FA 0B 5C 65 69 EB DE 4E DD 63 08 83 2C 10 40 D0 44 49 F9 AE 48 B7 D1 75 8D B7 45 EC 08 DA B1 B2 EE EF CF F1 A3 75 93 21 AA C1 50 5C 9B FA CF CD 99 34 26 9C D2 E9 AA 1C 13 8A 8A 0A 5E EE 62 D1 89 31 32 44 88 40 6D C6 BF CF 3B 36 FE BB FC 7F 2D 62 E1 0D 06 0C BD 79 2F 0D 8C 9D 16 44 FB EE 84 AF 39 67 5C BD A6 C1 BD 77 C6 78 81 AF F4 DB C7 3D E9 2D 2F 5F 6C 5F BD 7C D5 A6 E8 96 D4 F0 8E F4 0F 5C 34 E1 DD A5 F9 AB 0A 70 54 1C D8 B5 2E 9B A9 11 9E B7 F8 27 40 04 A1 1A 9B B7 29 99 CD E0 13 9E 5C C5 FD E5 3B DE C5 85 48 DE 21 5D D7 10 AD 77 81 B3 90 9E B2 A1 70 C0 45 AA E4 06 0F DB E4 F7 0B CB 25 28 C9 C1 EA 0B 23 E9 EC 16 45 2F 3B 07 2E 05 15 EA E8 89 C9 CE D7 BE 2B 2D 0F C1 BC 16 8A 4B 79 22 78 4C 70 50 46 86 78 C2 6E DB A2 A9 FA 93 F8 B4 3F 32 50 EF FE 42 EC F6 99 AD 5E 9A F0 E8 B5 F3 96 5C 0B A9 93 DC F2 D9 EB 1C D9 B1 38 E1 87 73 B8 48 DF 3E 35 74 68 E7 C7 94 D4 05 83 C2 08 90 9E 7C ED 61 1A 5A B7 32 5F 09 36 37 6F D3 7B 6F 67 38 D2 EB FD 6A 60 98 87 DE A7 61 88 74 99 80 C4 4E A9 9B 9F CA 4D AB 20 E3 FA 37 07 57 BF E5 A2 9D C5 CB F7 BA 56 E0 38 1B 3D AC DC 51 36 7C 60 E7 BA 00 FB 67 BC 48 6D 17 C6 EB E2 93 F3 2D 5B 16 AF 6B 83 CC 27 15 23 76 5A 73 64 21 C6 21 8D C8 9F 5C 3B 61 47 6F 96 3D EA 7B EE 12 FF 20 F3 20 09 AE 60 C0 86 47 D2 45 18 BC 73 1B 21 AF DA 02 28 4C 05 A9 69 52 31 F8 75 B4 47 A5 A9 49 70 A6 5D 33 F2 07 2A 20 AD 5E 31 6A FC F6 96 11 48 F5 9D 85 CD 97 A6 BF D0 28 C3 51 AA 62 90 98 AD 7E 94 73 53 2F 00 00 13 E0 + |00 00 00 0B 01 00 00 00 00 0E 11 11 11 11 11 11 11 11 11 11 4F 05 F9 A5 25 DD A9 68 1B 80 60 1C D5 63 2F 8D 04 46 D2 7F 20 C1 F4 91 99 FF 90 BD E6 81 8E 24 ED 86 12 2D A4 41 C9 99 4C 70 70 75 29 A7 7A 6C 8C 51 9A CE 2B 43 A1 3E C8 97 CD AB AE 25 21 1A C5 D2 6E 3B 1D F6 3A DF EA 4B 43 C9 5D 1F 16 B3 04 DC BF 6C B8 78 60 A1 56 C9 E9 13 23 77 A3 33 8B B6 88 BD E0 16 74 DE F5 CA BF 50 5E 7F 74 66 29 5D 00 56 E0 AD 04 EF 07 A6 FB E0 A1 85 0F 11 C8 BF F2 E7 C1 21 DA 71 B3 D6 42 A7 0D 8B BB 38 8F BF 2C 54 C8 24 14 0D E3 DB 77 FC 25 C3 D9 3A 12 10 12 E2 A7 E2 81 61 F7 88 BD 2E 05 C4 AC 84 DE 33 6B 9B 8A 9C 78 97 AB 15 4C AE F2 FB AF 93 94 45 69 69 66 8E 46 59 78 53 6A 12 CA 4C E6 AF B7 CB D7 CD C2 47 A6 71 2C 66 D8 76 4D 13 3D 8F B1 33 E9 D4 F1 2B 6B B9 E7 DD 37 E1 91 C0 19 C5 4F 73 C8 89 AC 71 CE BD 3C 64 25 5F FF 00 67 C7 5D 22 CA B1 53 D4 05 22 1A 05 FA 3E BD 13 A6 F9 4B 08 C9 68 4D C0 43 1B 84 AC 2B C2 EA D6 82 8B 28 A2 32 C4 E9 87 36 C0 E7 13 3E B3 CD D6 70 0C 2C 4F E6 F4 D4 5E 32 80 04 D1 CA 4F E6 A1 D3 C3 71 5D 57 18 8B 6A E4 B2 54 47 FA 95 8E BB 3B 92 99 94 33 86 D2 EC 36 69 78 67 BB 17 DB 58 C7 6C 2F 95 0F D8 E5 B0 B5 6F ED 90 88 46 2D 46 BE 23 BE F5 3C 89 AE FB 63 BC F8 C5 E2 5B 96 F4 58 7D 60 FB F8 4F B8 F0 66 3C 89 EF 16 4E 51 30 76 B0 6A 73 DE 95 D2 AA 26 B4 DC BA AB E1 18 17 A2 91 68 FC E0 C3 CE 74 EC F8 5A AE AB E4 4C 6D 66 C3 EF 73 A3 C2 8B 3B C0 32 37 DE 05 A5 EF 7B 2A FB 40 E5 1F E2 FE 0B 95 76 1C 41 87 FC 34 3C 84 B0 63 4D 5B C2 DA 57 08 68 69 3C CC AB CB 66 09 BA 6A 18 25 80 76 E2 20 B8 AC 9F 84 A1 EC D4 21 75 80 0E C1 3C 6B 19 6E BF D6 6A 38 83 C1 21 64 8E 09 CC DF C2 01 D6 68 6A F9 38 8A 6F B2 74 32 EA 8C 90 88 9E 8B CC 21 66 82 76 2D A5 09 27 AC 3F C9 71 BC 0C 86 57 14 37 BC 87 50 A1 F3 9D 9F 27 C2 2E F0 01 10 D6 7F A8 78 AC DB 93 A7 40 42 84 74 8D 15 08 6D 6F EA 02 E2 68 20 BE 3E 70 18 5D A8 08 BA 93 7C D7 0B 8C 76 C3 6C EB 2C 2D BD 59 70 63 83 3D 26 A6 76 D8 11 A7 A8 E9 AA BF D5 EA 99 F1 08 10 9E 31 58 F1 59 99 ED 94 EB 53 64 49 0E 0F 1D 7A 10 3C FA 19 06 AB 49 A8 EE A9 21 2A 29 86 87 C7 8C 31 C0 E9 CA 57 99 E3 DC 32 F7 89 21 C7 72 5B 58 85 B8 65 6B D6 56 D6 C2 9F E5 03 6E BB B4 9D 09 FE 34 A2 DE 3E 86 8F FD A9 6D 8C 86 BC 4F B6 0F 1B 16 1E C4 36 CD F4 80 63 13 36 A7 2F 49 22 42 3E E9 FD 8B 0B BC E0 BA 2A 28 B8 7B 05 A9 72 23 17 51 30 BE CC CB A3 0B 7F 73 A4 9A E7 DA E6 D3 B1 92 0A CF 43 F3 6D D6 16 01 F5 0F 69 2C 67 72 39 BB 1B 87 F8 52 B7 AD 2D 47 D0 82 FB 1F EB 36 CC CC 19 40 F5 EA C1 B2 76 C3 3B E9 0D 8C 3C 1A 05 91 B4 E9 AA 9B FF 40 EF E7 36 D8 94 B2 76 99 B6 27 4E E9 01 A3 E5 A3 CE F1 7B 4E 6D F2 53 05 33 9D 1A C5 3C BC F3 E8 BF 84 0F A8 21 C0 10 CD D1 26 1B 34 80 AE 7B 48 97 45 F6 DF 8F 79 C3 4E C6 47 34 E4 FD 0D F9 20 D2 62 20 FE 96 32 73 69 04 6F 44 8B 68 AA A6 30 4C 28 BB C1 B4 0B 6B BC EF CD DA CA 45 C2 5D 71 C2 53 DD B0 92 3D 0F 80 37 C2 A9 8D 89 E3 D9 11 1C 98 F2 7E 4A 9A EE 6D 50 E0 04 E3 97 71 F2 AC 8D 5D 8A FE 75 F0 B5 E8 AD 3A 25 4D 2F 0C 82 1D 94 5A 7C 63 32 3E E3 82 95 80 2A 6F BB BA 51 38 1C EF 9E DC 1D B2 31 A9 F9 AA 05 27 EB A9 4A 58 D1 21 DE 1C 55 88 3F A5 EB 27 F4 CF DA DF 67 7E 24 5C A0 B6 BC 1F 4E E1 3F 70 86 62 DB A1 BE 5A 88 C6 20 22 B3 F3 DD 9D 08 1B 78 15 55 EC 47 76 AB E6 4C 77 35 D3 08 9F 52 9B 9F 42 58 28 A4 8D 96 F0 BF C1 42 90 1C 18 A2 98 25 86 B0 9B 3B DE 75 C1 87 4F 9A 7E FD 03 9F 0F 95 E4 39 04 FE F4 B7 DA 57 BE F0 D8 09 B6 42 F4 CD CF 8E 38 98 6B 29 77 D3 19 63 03 2D CC DB 6C 54 85 18 5D FA AB FE CE 46 6A 1F 8B 63 0F 67 27 55 E7 F4 03 7F B1 52 7F 2E 01 F7 8C B9 9F F6 03 4F DD 05 D2 3A D7 2D C9 1C 3D A1 2E 60 7A 0A E5 7B 72 C7 8D 18 9A FA 4B 3F 16 23 17 21 6B BF 32 9B A2 E9 64 F2 E4 F9 B2 65 81 8A 99 F7 23 36 8D 57 E7 A6 26 26 1F 7F 19 F7 5C 5B C3 08 A1 D8 27 C3 BE 98 26 B3 50 63 F0 05 6A 29 19 05 CA 4A 12 CF 33 95 27 53 14 A2 5F D0 37 B6 03 0E 0C 72 44 E4 DB 89 B3 E0 D6 0D AA 3F 1F DF E8 86 E1 55 D2 6B 15 AA 98 A2 92 E9 1B 2E 20 8A 6B C8 6B 0F 22 37 FD F5 36 06 BB 3F 03 23 9F 40 08 1C 58 DE 20 74 BB C2 7B 0A 81 FB 6F D8 89 DB 03 DA 89 86 B1 40 A3 AB 8D A8 CD 2D 9B 97 98 20 6D 73 8F 35 F2 26 F2 8B A1 47 35 27 9D 85 BC 65 4E 7A 42 FC 39 1D 62 24 7C 1A B1 01 CC FA 40 15 FC 4B 2F CE 1B 9B 0B EB 07 F7 12 55 BD 7C B1 5C BC AC 53 16 DB 34 CF D3 91 6A 92 9E FA E8 A2 6B 4D 49 36 3F 33 79 49 4D 52 EC 1A 3F 0A EB 9E 61 E8 8D FA A2 D4 A2 E4 26 1B 2A A2 CA 6A 94 35 8D 77 CE 0A 7F 99 7D D8 53 61 28 F7 27 43 38 99 05 9E B9 7E 9C B5 61 84 AE 0D 9C 44 EF D1 54 B8 3F B0 E8 D4 FE EB 42 A5 DA 24 07 28 F5 21 93 A2 53 30 18 26 AB 3C 99 88 EB 0A C8 D4 6E 76 25 A7 65 13 59 16 FE C0 E7 23 BD 6D 4D 5B DC 4C 02 8A 93 56 B5 3D B0 ED 2A 09 CD 01 9A 5A A0 62 FF 4C 23 7E 1C 2B 33 AD 29 B8 7B 4F A6 71 7B 1A C1 3E 2D B9 2F 78 56 CF D2 65 EA 02 E9 1D 4A 27 CD 94 B0 67 79 E3 9B 2B 9C 50 1B 78 ED 06 3C 06 C0 07 6E 53 49 73 2C 74 59 AF A7 92 16 A3 DA 35 B7 B9 B8 D8 CF 58 3E BF 43 BF 43 E8 C5 B7 36 B0 7E 65 AF 6F 75 89 C5 76 87 40 19 9C 4F 30 54 18 B2 09 74 4B D6 A5 29 22 76 E8 19 7C A5 A9 B0 98 12 06 0A 9E 87 F3 1F 75 44 32 CD 47 FE 56 7F 84 4E 73 EF 28 A3 C6 78 08 94 1D 6A D2 E4 31 20 99 63 D4 F7 A8 84 CE C8 5E 43 45 01 3F BC 9A D3 0C B7 FF B0 A8 3D C7 EE D7 74 76 50 9C 61 67 E3 E2 19 BA DD CB AF 8F A2 CD 55 56 1C 32 5F 16 AA AE 24 2B 10 D4 A1 F9 2D 91 93 1F C5 BE 7D 5F EC 4B F4 E5 3D FF 10 3B F7 2D 27 F1 97 FF 46 BF 6E 48 1C 7B 49 BE 1F 7A F8 79 0A A4 C4 2D 73 C2 FA 1A 71 56 6E A7 6B 1A 46 83 DA 80 47 1D E7 FD 03 3A 98 51 E5 3D 30 71 05 41 D5 F5 8F 7E BC F1 47 6E D2 2E 9B 6C 18 30 D0 92 F1 BD 5F 67 D4 5D BA 75 2F C2 5F 98 64 5F 2C 7B 72 8F 77 F9 7D 07 F9 66 9B 21 6F B1 ED F0 22 B4 0E 4D 3B 79 42 CD 84 66 F7 6F 57 B4 CD EE 22 B0 DB 31 74 12 DC D8 B3 54 07 04 18 77 D8 80 D4 FD 33 AE 57 64 E1 23 37 68 4D 1A F0 36 ED C5 FA 97 20 93 8D 37 A3 A1 84 CE C9 4B 2B E8 15 CB 3D 9C 10 08 A1 FE 7E 51 B9 F7 F5 0A 8A 65 FF 6B 7C E6 01 2A ED BA E2 A7 26 32 F9 79 F5 2C 16 81 AA DD 15 1F D2 A4 C7 B7 26 7A 5A C6 CE EA 59 26 68 07 6F 10 E8 6B 3F 22 B3 B0 64 7B 05 AA 76 98 0B 03 1D 6F 25 B6 08 54 A8 87 8A 84 92 29 9A 2D A4 59 81 BF 52 DE 24 F9 5F C2 5A FA 1A D7 EE 03 6A 00 C3 8E 28 97 31 55 CC 07 1A 98 11 1E F3 E8 E3 5A 6B F1 91 87 4A 74 83 7E C1 DF 06 AF 2B C6 B0 B0 56 F6 96 D0 C4 AC F3 A8 7C D8 4A BF 50 66 09 AA D1 0F E8 8F 7D DE 70 56 8D D4 68 E3 1A BC C1 EA 9F 6C 8B F3 8D 04 D7 24 95 E5 E9 FC 72 36 D7 9D 01 C9 0B 81 46 01 4C B7 BB 48 36 B4 3E 52 3C D3 0E 24 36 64 94 CC 23 F9 A0 86 56 36 0E 67 B1 EF 86 1D 93 7F 58 3E B6 91 49 14 31 F6 EB C3 5A DF A8 C8 26 2E 29 37 7A 23 7C 3B D9 C0 53 33 52 02 AC 6B B5 FB F6 A1 6D 13 E4 87 CA 56 3A B2 0C 75 92 75 5C A0 1C C2 31 97 E8 1B 13 AC D7 E1 47 E3 E7 CE 86 FF 85 AE E2 BD 5C 6F 3F 65 FA 11 C6 C0 12 CE 86 19 C8 E2 72 8E B6 4E 88 73 E0 C1 31 86 42 F7 51 05 68 E0 73 99 0E 69 86 59 C9 4A 77 CA 76 10 AC DE 95 F9 17 4A 8A 5B 14 A8 16 57 C4 16 60 CF 0E 48 EE D0 DC 63 8B 9B 46 6D 96 41 1D 38 8B DF 70 31 95 3A F7 A8 1F B8 4D 7A BA E0 CC E9 58 B5 2A 87 42 43 63 DC 12 47 AC 32 49 D7 02 87 DB C9 5D 10 BC 93 65 F4 3C C3 FF C7 28 86 14 4D 4C 70 FD FE 5A 06 EA 01 C5 27 FA 46 9B 98 5E 71 D5 BE C1 0A A2 45 7F 09 1A 7D 12 4F 38 49 09 77 1B 8A 39 48 27 22 4C B1 A3 AA 68 8E 1B 73 B0 A6 EA 78 AA C3 33 16 3B 00 69 4A D5 07 06 42 65 50 2E F2 60 8B F0 9E 8A 7C 05 B7 DC 85 E7 EF 10 FD C8 FE 80 A9 C6 B0 46 35 0F 77 17 6C 7D EC 60 24 D9 21 76 AC DF 6B B2 FC B0 75 D1 3C 2D 69 60 C2 4E CD 62 58 72 D8 AC B3 F5 75 56 80 E3 D1 9B 95 1B E7 CD D1 C3 C2 FE 50 DC DD 10 D5 89 43 7E FB 90 54 4D 42 13 1A 47 20 DB 18 E7 BA 3D 2D AF 53 DC 8F 37 08 8C 3B 52 00 A4 62 AB 22 93 46 05 38 A8 C2 FF 2C F5 F8 5D B5 4B 04 7E 48 F7 03 75 DA C8 B0 E9 E0 0B 3B 7E 57 0A 1A 53 98 EE 1A 2A 48 C7 FD 22 D4 B0 E7 5C B3 75 8F B7 4E 8A EF B0 29 6C A9 C8 02 24 4E D6 3B 2C 82 4E F3 1E 62 94 40 64 2E 77 BB B9 3D 1E F4 BD DD BD FA DA 25 09 65 8D 5E 1B 3C 2E 80 22 11 D6 1D 32 B4 7E 89 B0 F6 85 83 C3 34 78 57 3C 97 DF 05 01 6C 8F 91 25 95 D8 BD D8 86 7C 8C F0 50 88 25 10 6D E6 E2 DA 76 31 80 55 2E E6 1F 37 A1 82 8B B5 FC 2B 71 5D 9B E6 89 CD 14 BB 49 28 BA 1D 49 71 08 A7 D2 B5 A4 1C 9C 1B DF EF C3 E7 55 2B F4 2E 4E 93 4A 74 B6 A9 47 18 23 9C 44 08 3D 13 06 24 41 06 1E BF 88 BC 3D 1B 75 92 80 C7 78 A4 CA 5A F1 79 EF 3F DC F5 5F AF 49 73 68 96 C9 DB 8E F2 A7 B4 2D 0F 50 64 0C EB EB F6 08 09 0A F5 0A 2C 88 5E AE 44 25 89 DF 7F 4C 5D 0C 73 47 3C 1A 2B 41 7B 9E 28 48 39 0A A2 D8 04 78 90 DE D5 88 9E C7 00 5F F8 63 CD BF 8D F1 5C 12 CE 7E 2B 09 D0 4C 42 7A 84 0F 36 C6 C9 5D AA 44 87 23 4A 76 76 94 1A 7A 2D D6 23 B4 84 EF 6D 3B C6 01 B2 F7 20 09 40 51 E4 80 58 5E A6 A5 97 2F 75 CE E7 97 78 F0 0B F9 31 98 CE 6C 40 4F A4 B3 14 AA A5 89 AA F9 9B 21 11 69 A8 E0 56 B0 F1 C9 CC 44 83 1C 32 E3 79 14 1A 5D C8 F3 41 C5 9A CE A1 B7 5B C2 53 22 49 15 FE 52 A9 38 DB BD 7A 69 83 06 0F 69 77 98 D1 F4 93 3C 83 37 B7 21 A5 D7 2A 9E D9 A3 BC 1C 9D 4F A4 22 51 4C 44 B9 35 C9 8E 41 FF 0D 6D FC BD 12 F5 CA 3E F9 EE 75 1D 01 9B CD 34 B2 C6 4A 4B 9D 2B 79 A5 12 52 BF FF F4 42 62 39 53 58 8E 82 F6 94 2C 00 97 0E 8A B3 5B 73 3D D6 CF 3E D5 14 BB 2D 5B E6 2D 78 88 D5 E8 33 BC E9 C9 A4 2D 36 41 F5 DA 5D 68 6E B6 B9 93 89 E0 15 E5 68 65 48 42 FA 9E 16 20 5A C8 CB 8D 61 52 E1 23 DE 0A 40 8C F5 6F 86 AD 36 F6 20 9E D4 6A B2 D9 7E 5E 3E F3 D7 C8 A8 25 CB 49 E5 FE BA A8 4E 34 41 B4 CB 9B AA BA 8F B9 53 F9 0A 67 AC 3A 02 D2 55 AB E3 6F 8C 79 E6 C0 2E 70 5C 6C E5 89 17 F2 A4 C8 8C 58 58 8C 3C 22 10 68 0D 9A 70 63 59 F6 C0 60 6F 1E B6 9E B5 69 B3 1B 0E D8 F8 8C 5D 06 2F 42 4F 69 BB 83 DF F8 20 8A BF 74 30 4A 57 C8 FE 13 AD 8B 09 B9 92 7A DD 53 DA A9 33 00 85 53 88 2E 58 3D A4 02 ED D0 E9 25 E2 EE 7A C0 08 B3 B5 BF 7D 36 07 0D 87 F7 6B 68 BF C6 7B A6 E4 B6 D7 76 FA BE 23 E6 53 94 D3 54 94 6D 39 87 3F 74 F3 ED 5C D6 E0 4F E8 5C AD 86 3E E7 E1 12 32 51 F0 E9 42 B5 12 46 26 B1 86 AD 19 1D A2 81 4E 3E F2 FF 8A 5F 9C 80 02 56 70 AA 0B 0D 2C 61 DA B9 1C 5A B3 6F 2E C5 32 C0 48 FE 19 9E F8 F4 30 3E E2 2C D8 A5 81 E9 C9 6B 44 6B B7 95 5D 94 0E C5 63 D7 78 FD 58 54 E1 19 E2 22 B5 48 10 D5 87 28 F7 A3 30 43 9D 69 B5 D5 38 D5 05 22 77 FC DC 93 62 08 CD 58 19 E4 65 E6 AC 05 6C FE C9 02 3A 28 48 77 AF D9 63 40 25 45 CF 89 4C 0A ED 60 C9 A8 75 A0 68 79 69 8C 13 5C 0C B4 FD 8F 27 8E BA 16 E5 1E AA 78 84 AF BE BB E9 09 9C 51 8A 10 2B DB 0E 7E F4 E2 A4 DE 15 85 A2 E2 40 E7 37 56 A5 AF 91 4E EA 80 E3 8F 3A F3 28 15 8D F4 FA D1 EF C0 09 98 43 1A A3 98 CA 94 BB DD EF 98 E1 98 6E AD 53 9F 48 E5 9E A5 1B 7A ED EE CC 5C 8B 05 AF B9 76 54 E1 C5 3A 91 00 A8 CE 1C AC D8 FC B6 2D 96 D3 CE E7 81 3F B0 E7 D5 39 C4 E0 E8 85 D4 18 51 9E 41 BA FE FA 32 0E 4C 4A 10 25 75 21 9E 0E 5D 29 A4 A0 6B F2 EA FF 70 9F 38 85 35 94 B6 90 39 DB 0D 25 E0 FF 89 97 93 94 88 84 8A BE D2 97 AE BF AC 2A 28 30 F4 A2 59 95 A1 F0 66 86 A5 2A 55 7A F0 35 0A CA CC DC 61 32 A8 B8 5C 47 90 19 91 E0 1E E1 1C BE FE A4 A3 02 27 7A 30 F6 10 11 CF ED 34 3F 18 74 E8 A5 23 C4 D9 73 5D 46 AA A5 EC BA 6E CE 40 EE E1 2D A0 9F CD DC B6 6D 6E CE 3A 94 A0 90 4D 7E DD 38 A9 52 27 3E 3D CF 94 C3 A7 07 F1 46 30 DB 0D BF 9F 30 12 25 FB 51 42 3F 74 49 3F 6C 1A 20 91 96 7C 1F 62 6C DA CA DE 99 48 74 D4 E2 5E E4 B8 AE BA 55 B5 95 E1 51 57 5B 0D B8 F9 4A 1F 86 CD FF AB 69 80 6F 65 76 DE 10 DF 91 EE 3F C4 69 81 76 6A 03 BB C5 73 5F 59 5A 0D BE 64 38 66 D0 75 EC 93 E7 52 E5 A6 3B 8F 99 8B A4 5E 6E B0 53 F5 4D 21 F4 D7 3E 7E 18 59 4F 83 56 E9 09 78 9C 90 9E 73 A2 87 F6 CC F4 DD 7C E5 24 BD DB E6 12 AC 48 28 96 98 B8 8F 92 20 F0 3D 22 0C 0B 34 93 9D D0 19 0E 9C 81 C2 FD A7 B1 09 56 E0 B8 C1 49 4F 76 9A 47 5A 69 17 32 99 61 76 05 F8 66 80 C3 D5 16 9D 78 88 C8 9F 2D 44 60 C5 44 18 C0 5F 90 59 52 9C 54 A8 C1 21 73 E9 B8 23 CC 8B A2 E9 E1 E9 6D 7A AF 2B 59 3E EB E3 E4 65 9C 5F 31 D3 EF 18 33 09 49 C8 C2 36 D5 4B 6C A4 79 66 B6 5B 8F C8 5F 74 88 5C 89 45 57 A8 1F 60 A0 2C D0 C9 6F 8D E5 D7 EF 64 47 B3 2F 5E D1 F7 A5 51 B2 E7 66 14 5C 9B 94 6B 47 C4 B1 D1 7E 25 01 70 9B E6 F9 02 B6 84 30 42 4C 9A 10 D9 95 FE F7 D1 60 05 9B CB 58 BE 92 A3 BD 7E B8 D2 CA E3 2D E3 EF 80 E5 86 64 BC BD BC 28 6D B8 C4 37 FB BD C3 5C BD 67 FA 22 98 79 22 5C 99 E2 90 02 FB 3F 09 10 A5 71 A3 95 17 AC 14 8F 6F 48 8F 39 B4 4E 23 78 2D FF FF 77 D9 E8 B9 F6 5D E0 39 44 92 C7 56 E9 25 F4 52 1C 6A CE 4C A2 BF 44 94 97 D4 42 3E 71 FC 59 68 16 67 AB DC 7A 4A 96 1E F1 F3 3E 85 F2 4C 2B 1E 2A 78 04 73 28 60 87 4B D9 4D 62 05 3C 33 DA 0B 13 90 EC 62 AF 6A 15 30 D2 46 1D 45 EA 4B 8A 23 7A D6 8B C5 89 6C 8E CE 6E 84 03 82 2D B5 19 E6 20 93 01 C2 24 84 74 23 79 F1 F4 99 FD 7C CC 0A B4 B4 18 5D 34 09 9F EE CF 6A 89 33 B3 DF D2 00 AC 4F 3E D9 A6 99 C8 13 A5 6B C8 2C 3C 30 B0 FD B7 13 CB FB C5 21 3B 1A EB 90 FA C1 4F 08 99 04 5D C5 8A 72 42 30 A4 49 88 5B 1C 31 14 1F 12 A6 8A 52 E9 1C 09 16 86 A2 81 19 30 50 9F A9 66 31 1B 6D 97 31 01 DF A9 77 F5 EB 59 AE 37 90 02 63 FE 85 25 79 3D A1 C2 30 12 83 50 3F 77 CA 2A 28 3C 87 84 C4 BE 4A D1 8E A4 A8 89 DD F7 21 03 9E 6C D3 6E AA 0A 25 27 8D 66 E3 C3 7E 17 95 0B CE E7 2C 37 55 80 85 CA 61 83 94 E3 63 0A 4F B0 CB C5 42 CE 0F 00 3D 4B 77 49 A7 96 22 E2 E0 AF 43 6B CE F2 D5 5F 0D F6 4F DB 37 D1 EE 8D 69 97 92 CD 2A BC 45 9D 3B 47 27 37 6B E3 DF 4B D8 FB A2 67 8F 7C 19 95 EE 98 E9 3A 05 28 8D C9 26 56 CB E0 4E A8 99 F9 CD 80 6B 65 18 B2 AD 66 E2 1A 14 7A 03 2D 24 E3 90 12 6A 25 05 2D 5C 32 7A 1C 13 D4 B6 F5 6D 4E A2 5D 71 32 88 50 1D 0F B6 DD 61 23 F7 DC BB 5C 27 B6 EB 7F 5B 89 5D 6E 8A 19 CF 36 D2 7F E6 55 0F E5 18 F9 9F 49 6D AC E8 5D 8C 2A 6B B7 D8 6F 00 ED 2A F0 B0 5F DE 96 D4 3D 23 C9 B1 75 EE 91 EC B6 31 DF 50 2C F9 C8 BD FD F7 A6 BC 98 02 BA 9C 23 24 6C A4 3E D6 2A 5A 65 4C 39 72 0C 8E 43 29 08 4C 21 B1 A2 29 76 6F 19 03 6A 2C 8D DB 9F 2D A5 6F 01 34 A6 6C 80 3D A1 2E 90 FA D5 56 A3 89 C0 B8 09 04 31 60 73 A5 7C 8F 04 92 7E A3 7F 60 24 B7 69 E8 5C 5B 53 55 D9 03 B6 32 6E 0B BB 2D 57 AC C5 7E 70 58 C3 4A CE 35 E9 26 AB F6 B4 B7 AE 5D 03 82 8B 30 D8 9D 30 9B 93 4C B0 48 D2 D6 3B 04 95 20 18 14 EC 75 58 E8 42 DE 1E C7 B9 3F BF 86 E9 3C 82 D8 62 0F 97 4D D1 D8 7D EE B1 1C C0 15 17 A8 D4 A9 D2 5B 2F 12 95 C2 7D E8 4C 51 6D 95 88 C8 33 7E 69 24 8F E4 2F 7F 22 20 32 BD 01 C8 71 C6 C1 A8 3C CA BC 8F 64 37 5A 4A 15 A6 36 F5 31 34 7E 70 78 5B 1C 8F 0B 14 28 9B 43 D1 49 3B F7 91 DC 12 F5 EF C6 0D 39 FE B4 6B 4D E1 BA 87 96 CB 75 87 B2 78 A1 D7 F6 7D DA ED 77 FE 42 DE 28 29 DF 1A 1F 11 CA 70 BC AF 9D 7D 5E 07 23 C8 8B 2C F7 1A 40 A2 15 73 2B 92 DE 98 57 E7 3D 43 A2 2A 8C 66 6A 0C 9D 15 A4 65 17 F5 A5 15 F7 CF 4B AE D0 46 E6 4E 62 E2 6E D9 FB 64 17 A8 96 57 8A 8B E8 89 C3 73 A8 FB 49 92 F6 4A 51 0A BF 2E 33 2E F6 6D BD C0 C7 0C 4F 69 8D 10 BB 46 C8 B4 88 0C D8 ED 2E 2B 0D 1E 77 E3 C6 52 E6 6D 56 80 5E A6 EA 34 86 5D 3B 3D EE 19 F4 62 9C BF 7F B1 6E 78 A1 75 58 EB 94 DB B9 1F 9A D9 72 EB 1E 60 58 87 46 B3 9D 2B 2B D7 04 8A 13 30 CD 35 16 C2 0E AB C3 93 B7 F4 F8 25 C5 BA 20 B5 62 64 A6 78 D7 E9 FD AD BC D0 69 F9 A7 AF C1 9D B7 8A B2 DA A6 CF F4 5E F4 FB 20 52 2A F2 3D 2F C1 CD A5 70 3A 8F 1C 4D 93 77 DC 35 1E 77 9F B9 15 7B 84 0A 7B 69 86 20 CC BA 7F 60 6D F8 D4 0E 97 9A 90 62 ED 9F BE FC 65 C6 A1 BD 58 D2 64 3A E8 73 69 37 B7 33 80 6F 5F D3 6B 7A 24 A5 62 A0 23 0A B9 51 7F AC E6 A0 E3 95 DA 61 97""" + .trimMargin().replace("\n", " ").hexToBytes() + + // 792 + val part2 = + """31 9D 14 F5 35 51 07 F7 6B 8E 3E 0B 5D C2 B9 C1 70 16 9E D2 2D 75 62 5E 62 82 CB 8B 22 A9 E9 DA 91 37 97 3B F9 95 AB 12 72 D1 7E 8C 87 2C A8 D1 AC CD 40 7B B1 5B 3E 4F 29 CA A6 D2 D8 9C 38 15 42 E6 66 35 9E C5 41 2C 4D BC 22 1F A1 AD 24 CE 4C C1 B2 E2 E0 A4 A4 DF E7 01 8B 10 90 C3 38 02 A9 0F 49 AA 8B 86 EC 58 B2 AA 79 6E FF 1E 1F 2A 35 3F 3D 96 A7 DC D0 F6 E5 18 4E EA C8 C5 72 76 3E 74 2E 12 05 78 6F FC BD 07 50 CA AF EE C2 11 00 00 02 88 + |00 00 00 0B 01 00 00 00 00 0E 11 11 11 11 11 11 11 11 11 11 7C CC CC 8A 82 A1 2F A3 F6 08 8D 5C 7B 64 F8 BB 4F 09 87 B1 7C E3 01 53 5F C0 32 A8 6E A4 0A 1F 49 38 E5 FD 7F 20 52 A3 1E 66 A5 43 85 C4 96 97 3C 8F 2A 33 98 D7 A6 90 50 37 BE 76 2D 94 CE 1F D8 63 07 DC D1 5A 6F F0 FA 1F 41 9E 74 BE 8D FC 61 2C 66 3F BB 4C FB 9A 02 53 FF BE E5 FD 52 B7 FD CE DB 80 C0 6A 55 14 31 31 0C 8A 7D 24 DC EC 8C 45 62 ED D9 F6 DA BB EA C5 6E 76 66 11 F4 CB 2B 3B C7 45 6C BE 6F ED 9E 1D 2B 8A ED 3B C1 5B 06 DC 75 58 C9 66 13 4C 7D EA C5 F9 A4 D1 37 EF 2B BE 98 E3 37 1C 30 17 20 80 98 AD 5F BC 85 98 5B A1 7F 9E AE 54 0C 36 B0 6F FA 4A 5B 6F DC 7E B1 9E 99 F6 07 16 43 11 7A BC 82 F7 FB B2 95 7C 29 37 AF 38 70 6E A8 75 12 84 73 CF 15 A3 65 B9 2B E5 0A 01 30 67 A2 D2 BF 9F 30 AF 4D A5 A6 EA 4A B3 E4 BB AC 15 E3 FB CD FC B0 52 69 05 8D BE A0 11 1D A7 AB 65 6D F1 68 06 71 89 4F 0A 7E F4 68 A0 71 21 07 E4 85 41 76 16 07 78 2E C5 30 A6 2F 5D C7 DB EF 24 61 20 E6 6D FF 01 28 0D D6 73 42 BF F1 65 EA 7E DE 81 94 89 EB D0 65 8D 94 39 BE 89 7F B0 B3 0C 74 2F FA B4 2B 86 22 08 67 EB FA 62 83 FA BD CA 9D 92 95 A6 B0 BB 0C 76 07 C4 DC B9 E0 CD 67 AB 84 4D CC 55 52 E5 22 CD FD AC 08 35 81 36 9A 2D 7B 56 66 E8 B4 D9 A5 AA 7A 57 F2 F6 FB 20 A1 78 D5 5D F3 7C E4 7F 34 53 59 52 38 06 35 AD 6D 7A 97 99 AA 97 1D 9D 4A E4 22 7A 11 0C EA 16 27 7C 8A 9E F2 5A 82 D5 23 C3 01 BF D7 43 99 E7 26 25 AA 5A 27 29 ED 84 7F 9D 46 47 F8 C3 94 94 D7 C8 27 9F F7 93 16 4E 57 62 FC AC 32 2E A8 9F B2 62 EA 45 B0 53 A2 6C 52 D2 76 5F 48 00 93 5B 95 24 1F 3C E7 EC 40 26 66 0F C7 7D 30 91 A5 F2 6A 2F 2D CB 24 0B A3 F8 71 23 CA 31 A1 42 17 AC 9B 25 5A 2E 46 2D BC 34 DE 70 DA EF 4C E1 2D AE 2A 88 66 19 EE 35 02 C6 68 2C C4 0A 90 82 CA AB 42 D6 57 01 62 22 0D DF 65 6A 7E 6E 68 07 1D E6 1E DB E3 E6 22 F2 9A 36 F4 25 06 FA 91 83 9C 98 EC FB FD F0 F9 62 97 E6 37 30 F3 B3 09 46 D9 7D 7F D5 51 FF 8A FA B7 33 C9 15 AC D3 93 B7 8F 36 E8 1A 85 42 E5 C5 00 E6 28 9C FA 6C""" + .trimMargin().replace("\n", " ").hexToBytes() + + // packets sizes: 748, 5084 644 + + println("part1.size = ${part1.size}") + println("part2.size = ${part2.size}") + + reader.offer(buildPacket { writeFully(part1) }) + + assertEquals(1, received.size) + assertEquals(1, reader.getBufferedPackets().size) + assertEquals(5084 - (5696 - 4 - 748 - 4), reader.getMissingLength().toInt()) + + assertEquals( + """00 00 00 0B 01 00 00 00 00 0E 11 11 11 11 11 11 11 11 11 11 6D A0 E3 2B 65 53 C5 AE 22 D0 7B AC 3D DD 3C 6E FF 24 84 38 F0 B7 A3 DE 5F 3A 15 DE 7E 6D 08 D1 8C 18 A9 F2 89 03 03 43 15 C6 32 01 74 5B DD A4 33 49 47 78 E0 5B 83 2C E7 3A E1 CC 50 9C A8 8C 5C 88 06 A0 90 04 6E 23 6F AD 84 D8 6B 10 64 AD 33 5B 3F B5 3C C3 24 6C BB 28 8C BC A2 BD 5E 91 EA FA FE 5C 3B C3 F4 3B 59 24 37 1C E2 13 DB 75 C1 7C D5 8B 2F 57 AD C0 16 13 97 12 60 D5 4C 21 E8 13 72 B3 F6 98 05 89 BA 49 60 F1 1C D9 6A 82 F6 A1 AE 78 82 48 60 A9 24 3C A7 7A 93 79 96 8B AA E2 FA DE 5F E9 12 FE 51 27 47 CB 6A 20 DB 64 22 B4 3A B4 C8 5E D0 45 31 9F 63 9B 61 A4 F9 56 CB 0B BF 27 21 55 99 2A 53 EA E4 DC EA D3 7E 62 B9 1D 2A 48 54 C3 6B D8 E0 CD A5 CC 84 AF A8 91 05 2D AC C3 64 82 5F 56 EA FB 13 5B A7 78 ED B1 E1 ED 25 74 1D EE 5D A3 5F 2E FA 0B 5C 65 69 EB DE 4E DD 63 08 83 2C 10 40 D0 44 49 F9 AE 48 B7 D1 75 8D B7 45 EC 08 DA B1 B2 EE EF CF F1 A3 75 93 21 AA C1 50 5C 9B FA CF CD 99 34 26 9C D2 E9 AA 1C 13 8A 8A 0A 5E EE 62 D1 89 31 32 44 88 40 6D C6 BF CF 3B 36 FE BB FC 7F 2D 62 E1 0D 06 0C BD 79 2F 0D 8C 9D 16 44 FB EE 84 AF 39 67 5C BD A6 C1 BD 77 C6 78 81 AF F4 DB C7 3D E9 2D 2F 5F 6C 5F BD 7C D5 A6 E8 96 D4 F0 8E F4 0F 5C 34 E1 DD A5 F9 AB 0A 70 54 1C D8 B5 2E 9B A9 11 9E B7 F8 27 40 04 A1 1A 9B B7 29 99 CD E0 13 9E 5C C5 FD E5 3B DE C5 85 48 DE 21 5D D7 10 AD 77 81 B3 90 9E B2 A1 70 C0 45 AA E4 06 0F DB E4 F7 0B CB 25 28 C9 C1 EA 0B 23 E9 EC 16 45 2F 3B 07 2E 05 15 EA E8 89 C9 CE D7 BE 2B 2D 0F C1 BC 16 8A 4B 79 22 78 4C 70 50 46 86 78 C2 6E DB A2 A9 FA 93 F8 B4 3F 32 50 EF FE 42 EC F6 99 AD 5E 9A F0 E8 B5 F3 96 5C 0B A9 93 DC F2 D9 EB 1C D9 B1 38 E1 87 73 B8 48 DF 3E 35 74 68 E7 C7 94 D4 05 83 C2 08 90 9E 7C ED 61 1A 5A B7 32 5F 09 36 37 6F D3 7B 6F 67 38 D2 EB FD 6A 60 98 87 DE A7 61 88 74 99 80 C4 4E A9 9B 9F CA 4D AB 20 E3 FA 37 07 57 BF E5 A2 9D C5 CB F7 BA 56 E0 38 1B 3D AC DC 51 36 7C 60 E7 BA 00 FB 67 BC 48 6D 17 C6 EB E2 93 F3 2D 5B 16 AF 6B 83 CC 27 15 23 76 5A 73 64 21 C6 21 8D C8 9F 5C 3B 61 47 6F 96 3D EA 7B EE 12 FF 20 F3 20 09 AE 60 C0 86 47 D2 45 18 BC 73 1B 21 AF DA 02 28 4C 05 A9 69 52 31 F8 75 B4 47 A5 A9 49 70 A6 5D 33 F2 07 2A 20 AD 5E 31 6A FC F6 96 11 48 F5 9D 85 CD 97 A6 BF D0 28 C3 51 AA 62 90 98 AD 7E 94 73 53 2F""", + received[0].toUHexString() + ) + + reader.offer(buildPacket { writeFully(part2) }) + + assertEquals(3, received.size) + assertEquals(0, reader.getBufferedPackets().size) + assertEquals(0, reader.getMissingLength().toInt()) + + assertEquals( + """00 00 00 0B 01 00 00 00 00 0E 11 11 11 11 11 11 11 11 11 11 4F 05 F9 A5 25 DD A9 68 1B 80 60 1C D5 63 2F 8D 04 46 D2 7F 20 C1 F4 91 99 FF 90 BD E6 81 8E 24 ED 86 12 2D A4 41 C9 99 4C 70 70 75 29 A7 7A 6C 8C 51 9A CE 2B 43 A1 3E C8 97 CD AB AE 25 21 1A C5 D2 6E 3B 1D F6 3A DF EA 4B 43 C9 5D 1F 16 B3 04 DC BF 6C B8 78 60 A1 56 C9 E9 13 23 77 A3 33 8B B6 88 BD E0 16 74 DE F5 CA BF 50 5E 7F 74 66 29 5D 00 56 E0 AD 04 EF 07 A6 FB E0 A1 85 0F 11 C8 BF F2 E7 C1 21 DA 71 B3 D6 42 A7 0D 8B BB 38 8F BF 2C 54 C8 24 14 0D E3 DB 77 FC 25 C3 D9 3A 12 10 12 E2 A7 E2 81 61 F7 88 BD 2E 05 C4 AC 84 DE 33 6B 9B 8A 9C 78 97 AB 15 4C AE F2 FB AF 93 94 45 69 69 66 8E 46 59 78 53 6A 12 CA 4C E6 AF B7 CB D7 CD C2 47 A6 71 2C 66 D8 76 4D 13 3D 8F B1 33 E9 D4 F1 2B 6B B9 E7 DD 37 E1 91 C0 19 C5 4F 73 C8 89 AC 71 CE BD 3C 64 25 5F FF 00 67 C7 5D 22 CA B1 53 D4 05 22 1A 05 FA 3E BD 13 A6 F9 4B 08 C9 68 4D C0 43 1B 84 AC 2B C2 EA D6 82 8B 28 A2 32 C4 E9 87 36 C0 E7 13 3E B3 CD D6 70 0C 2C 4F E6 F4 D4 5E 32 80 04 D1 CA 4F E6 A1 D3 C3 71 5D 57 18 8B 6A E4 B2 54 47 FA 95 8E BB 3B 92 99 94 33 86 D2 EC 36 69 78 67 BB 17 DB 58 C7 6C 2F 95 0F D8 E5 B0 B5 6F ED 90 88 46 2D 46 BE 23 BE F5 3C 89 AE FB 63 BC F8 C5 E2 5B 96 F4 58 7D 60 FB F8 4F B8 F0 66 3C 89 EF 16 4E 51 30 76 B0 6A 73 DE 95 D2 AA 26 B4 DC BA AB E1 18 17 A2 91 68 FC E0 C3 CE 74 EC F8 5A AE AB E4 4C 6D 66 C3 EF 73 A3 C2 8B 3B C0 32 37 DE 05 A5 EF 7B 2A FB 40 E5 1F E2 FE 0B 95 76 1C 41 87 FC 34 3C 84 B0 63 4D 5B C2 DA 57 08 68 69 3C CC AB CB 66 09 BA 6A 18 25 80 76 E2 20 B8 AC 9F 84 A1 EC D4 21 75 80 0E C1 3C 6B 19 6E BF D6 6A 38 83 C1 21 64 8E 09 CC DF C2 01 D6 68 6A F9 38 8A 6F B2 74 32 EA 8C 90 88 9E 8B CC 21 66 82 76 2D A5 09 27 AC 3F C9 71 BC 0C 86 57 14 37 BC 87 50 A1 F3 9D 9F 27 C2 2E F0 01 10 D6 7F A8 78 AC DB 93 A7 40 42 84 74 8D 15 08 6D 6F EA 02 E2 68 20 BE 3E 70 18 5D A8 08 BA 93 7C D7 0B 8C 76 C3 6C EB 2C 2D BD 59 70 63 83 3D 26 A6 76 D8 11 A7 A8 E9 AA BF D5 EA 99 F1 08 10 9E 31 58 F1 59 99 ED 94 EB 53 64 49 0E 0F 1D 7A 10 3C FA 19 06 AB 49 A8 EE A9 21 2A 29 86 87 C7 8C 31 C0 E9 CA 57 99 E3 DC 32 F7 89 21 C7 72 5B 58 85 B8 65 6B D6 56 D6 C2 9F E5 03 6E BB B4 9D 09 FE 34 A2 DE 3E 86 8F FD A9 6D 8C 86 BC 4F B6 0F 1B 16 1E C4 36 CD F4 80 63 13 36 A7 2F 49 22 42 3E E9 FD 8B 0B BC E0 BA 2A 28 B8 7B 05 A9 72 23 17 51 30 BE CC CB A3 0B 7F 73 A4 9A E7 DA E6 D3 B1 92 0A CF 43 F3 6D D6 16 01 F5 0F 69 2C 67 72 39 BB 1B 87 F8 52 B7 AD 2D 47 D0 82 FB 1F EB 36 CC CC 19 40 F5 EA C1 B2 76 C3 3B E9 0D 8C 3C 1A 05 91 B4 E9 AA 9B FF 40 EF E7 36 D8 94 B2 76 99 B6 27 4E E9 01 A3 E5 A3 CE F1 7B 4E 6D F2 53 05 33 9D 1A C5 3C BC F3 E8 BF 84 0F A8 21 C0 10 CD D1 26 1B 34 80 AE 7B 48 97 45 F6 DF 8F 79 C3 4E C6 47 34 E4 FD 0D F9 20 D2 62 20 FE 96 32 73 69 04 6F 44 8B 68 AA A6 30 4C 28 BB C1 B4 0B 6B BC EF CD DA CA 45 C2 5D 71 C2 53 DD B0 92 3D 0F 80 37 C2 A9 8D 89 E3 D9 11 1C 98 F2 7E 4A 9A EE 6D 50 E0 04 E3 97 71 F2 AC 8D 5D 8A FE 75 F0 B5 E8 AD 3A 25 4D 2F 0C 82 1D 94 5A 7C 63 32 3E E3 82 95 80 2A 6F BB BA 51 38 1C EF 9E DC 1D B2 31 A9 F9 AA 05 27 EB A9 4A 58 D1 21 DE 1C 55 88 3F A5 EB 27 F4 CF DA DF 67 7E 24 5C A0 B6 BC 1F 4E E1 3F 70 86 62 DB A1 BE 5A 88 C6 20 22 B3 F3 DD 9D 08 1B 78 15 55 EC 47 76 AB E6 4C 77 35 D3 08 9F 52 9B 9F 42 58 28 A4 8D 96 F0 BF C1 42 90 1C 18 A2 98 25 86 B0 9B 3B DE 75 C1 87 4F 9A 7E FD 03 9F 0F 95 E4 39 04 FE F4 B7 DA 57 BE F0 D8 09 B6 42 F4 CD CF 8E 38 98 6B 29 77 D3 19 63 03 2D CC DB 6C 54 85 18 5D FA AB FE CE 46 6A 1F 8B 63 0F 67 27 55 E7 F4 03 7F B1 52 7F 2E 01 F7 8C B9 9F F6 03 4F DD 05 D2 3A D7 2D C9 1C 3D A1 2E 60 7A 0A E5 7B 72 C7 8D 18 9A FA 4B 3F 16 23 17 21 6B BF 32 9B A2 E9 64 F2 E4 F9 B2 65 81 8A 99 F7 23 36 8D 57 E7 A6 26 26 1F 7F 19 F7 5C 5B C3 08 A1 D8 27 C3 BE 98 26 B3 50 63 F0 05 6A 29 19 05 CA 4A 12 CF 33 95 27 53 14 A2 5F D0 37 B6 03 0E 0C 72 44 E4 DB 89 B3 E0 D6 0D AA 3F 1F DF E8 86 E1 55 D2 6B 15 AA 98 A2 92 E9 1B 2E 20 8A 6B C8 6B 0F 22 37 FD F5 36 06 BB 3F 03 23 9F 40 08 1C 58 DE 20 74 BB C2 7B 0A 81 FB 6F D8 89 DB 03 DA 89 86 B1 40 A3 AB 8D A8 CD 2D 9B 97 98 20 6D 73 8F 35 F2 26 F2 8B A1 47 35 27 9D 85 BC 65 4E 7A 42 FC 39 1D 62 24 7C 1A B1 01 CC FA 40 15 FC 4B 2F CE 1B 9B 0B EB 07 F7 12 55 BD 7C B1 5C BC AC 53 16 DB 34 CF D3 91 6A 92 9E FA E8 A2 6B 4D 49 36 3F 33 79 49 4D 52 EC 1A 3F 0A EB 9E 61 E8 8D FA A2 D4 A2 E4 26 1B 2A A2 CA 6A 94 35 8D 77 CE 0A 7F 99 7D D8 53 61 28 F7 27 43 38 99 05 9E B9 7E 9C B5 61 84 AE 0D 9C 44 EF D1 54 B8 3F B0 E8 D4 FE EB 42 A5 DA 24 07 28 F5 21 93 A2 53 30 18 26 AB 3C 99 88 EB 0A C8 D4 6E 76 25 A7 65 13 59 16 FE C0 E7 23 BD 6D 4D 5B DC 4C 02 8A 93 56 B5 3D B0 ED 2A 09 CD 01 9A 5A A0 62 FF 4C 23 7E 1C 2B 33 AD 29 B8 7B 4F A6 71 7B 1A C1 3E 2D B9 2F 78 56 CF D2 65 EA 02 E9 1D 4A 27 CD 94 B0 67 79 E3 9B 2B 9C 50 1B 78 ED 06 3C 06 C0 07 6E 53 49 73 2C 74 59 AF A7 92 16 A3 DA 35 B7 B9 B8 D8 CF 58 3E BF 43 BF 43 E8 C5 B7 36 B0 7E 65 AF 6F 75 89 C5 76 87 40 19 9C 4F 30 54 18 B2 09 74 4B D6 A5 29 22 76 E8 19 7C A5 A9 B0 98 12 06 0A 9E 87 F3 1F 75 44 32 CD 47 FE 56 7F 84 4E 73 EF 28 A3 C6 78 08 94 1D 6A D2 E4 31 20 99 63 D4 F7 A8 84 CE C8 5E 43 45 01 3F BC 9A D3 0C B7 FF B0 A8 3D C7 EE D7 74 76 50 9C 61 67 E3 E2 19 BA DD CB AF 8F A2 CD 55 56 1C 32 5F 16 AA AE 24 2B 10 D4 A1 F9 2D 91 93 1F C5 BE 7D 5F EC 4B F4 E5 3D FF 10 3B F7 2D 27 F1 97 FF 46 BF 6E 48 1C 7B 49 BE 1F 7A F8 79 0A A4 C4 2D 73 C2 FA 1A 71 56 6E A7 6B 1A 46 83 DA 80 47 1D E7 FD 03 3A 98 51 E5 3D 30 71 05 41 D5 F5 8F 7E BC F1 47 6E D2 2E 9B 6C 18 30 D0 92 F1 BD 5F 67 D4 5D BA 75 2F C2 5F 98 64 5F 2C 7B 72 8F 77 F9 7D 07 F9 66 9B 21 6F B1 ED F0 22 B4 0E 4D 3B 79 42 CD 84 66 F7 6F 57 B4 CD EE 22 B0 DB 31 74 12 DC D8 B3 54 07 04 18 77 D8 80 D4 FD 33 AE 57 64 E1 23 37 68 4D 1A F0 36 ED C5 FA 97 20 93 8D 37 A3 A1 84 CE C9 4B 2B E8 15 CB 3D 9C 10 08 A1 FE 7E 51 B9 F7 F5 0A 8A 65 FF 6B 7C E6 01 2A ED BA E2 A7 26 32 F9 79 F5 2C 16 81 AA DD 15 1F D2 A4 C7 B7 26 7A 5A C6 CE EA 59 26 68 07 6F 10 E8 6B 3F 22 B3 B0 64 7B 05 AA 76 98 0B 03 1D 6F 25 B6 08 54 A8 87 8A 84 92 29 9A 2D A4 59 81 BF 52 DE 24 F9 5F C2 5A FA 1A D7 EE 03 6A 00 C3 8E 28 97 31 55 CC 07 1A 98 11 1E F3 E8 E3 5A 6B F1 91 87 4A 74 83 7E C1 DF 06 AF 2B C6 B0 B0 56 F6 96 D0 C4 AC F3 A8 7C D8 4A BF 50 66 09 AA D1 0F E8 8F 7D DE 70 56 8D D4 68 E3 1A BC C1 EA 9F 6C 8B F3 8D 04 D7 24 95 E5 E9 FC 72 36 D7 9D 01 C9 0B 81 46 01 4C B7 BB 48 36 B4 3E 52 3C D3 0E 24 36 64 94 CC 23 F9 A0 86 56 36 0E 67 B1 EF 86 1D 93 7F 58 3E B6 91 49 14 31 F6 EB C3 5A DF A8 C8 26 2E 29 37 7A 23 7C 3B D9 C0 53 33 52 02 AC 6B B5 FB F6 A1 6D 13 E4 87 CA 56 3A B2 0C 75 92 75 5C A0 1C C2 31 97 E8 1B 13 AC D7 E1 47 E3 E7 CE 86 FF 85 AE E2 BD 5C 6F 3F 65 FA 11 C6 C0 12 CE 86 19 C8 E2 72 8E B6 4E 88 73 E0 C1 31 86 42 F7 51 05 68 E0 73 99 0E 69 86 59 C9 4A 77 CA 76 10 AC DE 95 F9 17 4A 8A 5B 14 A8 16 57 C4 16 60 CF 0E 48 EE D0 DC 63 8B 9B 46 6D 96 41 1D 38 8B DF 70 31 95 3A F7 A8 1F B8 4D 7A BA E0 CC E9 58 B5 2A 87 42 43 63 DC 12 47 AC 32 49 D7 02 87 DB C9 5D 10 BC 93 65 F4 3C C3 FF C7 28 86 14 4D 4C 70 FD FE 5A 06 EA 01 C5 27 FA 46 9B 98 5E 71 D5 BE C1 0A A2 45 7F 09 1A 7D 12 4F 38 49 09 77 1B 8A 39 48 27 22 4C B1 A3 AA 68 8E 1B 73 B0 A6 EA 78 AA C3 33 16 3B 00 69 4A D5 07 06 42 65 50 2E F2 60 8B F0 9E 8A 7C 05 B7 DC 85 E7 EF 10 FD C8 FE 80 A9 C6 B0 46 35 0F 77 17 6C 7D EC 60 24 D9 21 76 AC DF 6B B2 FC B0 75 D1 3C 2D 69 60 C2 4E CD 62 58 72 D8 AC B3 F5 75 56 80 E3 D1 9B 95 1B E7 CD D1 C3 C2 FE 50 DC DD 10 D5 89 43 7E FB 90 54 4D 42 13 1A 47 20 DB 18 E7 BA 3D 2D AF 53 DC 8F 37 08 8C 3B 52 00 A4 62 AB 22 93 46 05 38 A8 C2 FF 2C F5 F8 5D B5 4B 04 7E 48 F7 03 75 DA C8 B0 E9 E0 0B 3B 7E 57 0A 1A 53 98 EE 1A 2A 48 C7 FD 22 D4 B0 E7 5C B3 75 8F B7 4E 8A EF B0 29 6C A9 C8 02 24 4E D6 3B 2C 82 4E F3 1E 62 94 40 64 2E 77 BB B9 3D 1E F4 BD DD BD FA DA 25 09 65 8D 5E 1B 3C 2E 80 22 11 D6 1D 32 B4 7E 89 B0 F6 85 83 C3 34 78 57 3C 97 DF 05 01 6C 8F 91 25 95 D8 BD D8 86 7C 8C F0 50 88 25 10 6D E6 E2 DA 76 31 80 55 2E E6 1F 37 A1 82 8B B5 FC 2B 71 5D 9B E6 89 CD 14 BB 49 28 BA 1D 49 71 08 A7 D2 B5 A4 1C 9C 1B DF EF C3 E7 55 2B F4 2E 4E 93 4A 74 B6 A9 47 18 23 9C 44 08 3D 13 06 24 41 06 1E BF 88 BC 3D 1B 75 92 80 C7 78 A4 CA 5A F1 79 EF 3F DC F5 5F AF 49 73 68 96 C9 DB 8E F2 A7 B4 2D 0F 50 64 0C EB EB F6 08 09 0A F5 0A 2C 88 5E AE 44 25 89 DF 7F 4C 5D 0C 73 47 3C 1A 2B 41 7B 9E 28 48 39 0A A2 D8 04 78 90 DE D5 88 9E C7 00 5F F8 63 CD BF 8D F1 5C 12 CE 7E 2B 09 D0 4C 42 7A 84 0F 36 C6 C9 5D AA 44 87 23 4A 76 76 94 1A 7A 2D D6 23 B4 84 EF 6D 3B C6 01 B2 F7 20 09 40 51 E4 80 58 5E A6 A5 97 2F 75 CE E7 97 78 F0 0B F9 31 98 CE 6C 40 4F A4 B3 14 AA A5 89 AA F9 9B 21 11 69 A8 E0 56 B0 F1 C9 CC 44 83 1C 32 E3 79 14 1A 5D C8 F3 41 C5 9A CE A1 B7 5B C2 53 22 49 15 FE 52 A9 38 DB BD 7A 69 83 06 0F 69 77 98 D1 F4 93 3C 83 37 B7 21 A5 D7 2A 9E D9 A3 BC 1C 9D 4F A4 22 51 4C 44 B9 35 C9 8E 41 FF 0D 6D FC BD 12 F5 CA 3E F9 EE 75 1D 01 9B CD 34 B2 C6 4A 4B 9D 2B 79 A5 12 52 BF FF F4 42 62 39 53 58 8E 82 F6 94 2C 00 97 0E 8A B3 5B 73 3D D6 CF 3E D5 14 BB 2D 5B E6 2D 78 88 D5 E8 33 BC E9 C9 A4 2D 36 41 F5 DA 5D 68 6E B6 B9 93 89 E0 15 E5 68 65 48 42 FA 9E 16 20 5A C8 CB 8D 61 52 E1 23 DE 0A 40 8C F5 6F 86 AD 36 F6 20 9E D4 6A B2 D9 7E 5E 3E F3 D7 C8 A8 25 CB 49 E5 FE BA A8 4E 34 41 B4 CB 9B AA BA 8F B9 53 F9 0A 67 AC 3A 02 D2 55 AB E3 6F 8C 79 E6 C0 2E 70 5C 6C E5 89 17 F2 A4 C8 8C 58 58 8C 3C 22 10 68 0D 9A 70 63 59 F6 C0 60 6F 1E B6 9E B5 69 B3 1B 0E D8 F8 8C 5D 06 2F 42 4F 69 BB 83 DF F8 20 8A BF 74 30 4A 57 C8 FE 13 AD 8B 09 B9 92 7A DD 53 DA A9 33 00 85 53 88 2E 58 3D A4 02 ED D0 E9 25 E2 EE 7A C0 08 B3 B5 BF 7D 36 07 0D 87 F7 6B 68 BF C6 7B A6 E4 B6 D7 76 FA BE 23 E6 53 94 D3 54 94 6D 39 87 3F 74 F3 ED 5C D6 E0 4F E8 5C AD 86 3E E7 E1 12 32 51 F0 E9 42 B5 12 46 26 B1 86 AD 19 1D A2 81 4E 3E F2 FF 8A 5F 9C 80 02 56 70 AA 0B 0D 2C 61 DA B9 1C 5A B3 6F 2E C5 32 C0 48 FE 19 9E F8 F4 30 3E E2 2C D8 A5 81 E9 C9 6B 44 6B B7 95 5D 94 0E C5 63 D7 78 FD 58 54 E1 19 E2 22 B5 48 10 D5 87 28 F7 A3 30 43 9D 69 B5 D5 38 D5 05 22 77 FC DC 93 62 08 CD 58 19 E4 65 E6 AC 05 6C FE C9 02 3A 28 48 77 AF D9 63 40 25 45 CF 89 4C 0A ED 60 C9 A8 75 A0 68 79 69 8C 13 5C 0C B4 FD 8F 27 8E BA 16 E5 1E AA 78 84 AF BE BB E9 09 9C 51 8A 10 2B DB 0E 7E F4 E2 A4 DE 15 85 A2 E2 40 E7 37 56 A5 AF 91 4E EA 80 E3 8F 3A F3 28 15 8D F4 FA D1 EF C0 09 98 43 1A A3 98 CA 94 BB DD EF 98 E1 98 6E AD 53 9F 48 E5 9E A5 1B 7A ED EE CC 5C 8B 05 AF B9 76 54 E1 C5 3A 91 00 A8 CE 1C AC D8 FC B6 2D 96 D3 CE E7 81 3F B0 E7 D5 39 C4 E0 E8 85 D4 18 51 9E 41 BA FE FA 32 0E 4C 4A 10 25 75 21 9E 0E 5D 29 A4 A0 6B F2 EA FF 70 9F 38 85 35 94 B6 90 39 DB 0D 25 E0 FF 89 97 93 94 88 84 8A BE D2 97 AE BF AC 2A 28 30 F4 A2 59 95 A1 F0 66 86 A5 2A 55 7A F0 35 0A CA CC DC 61 32 A8 B8 5C 47 90 19 91 E0 1E E1 1C BE FE A4 A3 02 27 7A 30 F6 10 11 CF ED 34 3F 18 74 E8 A5 23 C4 D9 73 5D 46 AA A5 EC BA 6E CE 40 EE E1 2D A0 9F CD DC B6 6D 6E CE 3A 94 A0 90 4D 7E DD 38 A9 52 27 3E 3D CF 94 C3 A7 07 F1 46 30 DB 0D BF 9F 30 12 25 FB 51 42 3F 74 49 3F 6C 1A 20 91 96 7C 1F 62 6C DA CA DE 99 48 74 D4 E2 5E E4 B8 AE BA 55 B5 95 E1 51 57 5B 0D B8 F9 4A 1F 86 CD FF AB 69 80 6F 65 76 DE 10 DF 91 EE 3F C4 69 81 76 6A 03 BB C5 73 5F 59 5A 0D BE 64 38 66 D0 75 EC 93 E7 52 E5 A6 3B 8F 99 8B A4 5E 6E B0 53 F5 4D 21 F4 D7 3E 7E 18 59 4F 83 56 E9 09 78 9C 90 9E 73 A2 87 F6 CC F4 DD 7C E5 24 BD DB E6 12 AC 48 28 96 98 B8 8F 92 20 F0 3D 22 0C 0B 34 93 9D D0 19 0E 9C 81 C2 FD A7 B1 09 56 E0 B8 C1 49 4F 76 9A 47 5A 69 17 32 99 61 76 05 F8 66 80 C3 D5 16 9D 78 88 C8 9F 2D 44 60 C5 44 18 C0 5F 90 59 52 9C 54 A8 C1 21 73 E9 B8 23 CC 8B A2 E9 E1 E9 6D 7A AF 2B 59 3E EB E3 E4 65 9C 5F 31 D3 EF 18 33 09 49 C8 C2 36 D5 4B 6C A4 79 66 B6 5B 8F C8 5F 74 88 5C 89 45 57 A8 1F 60 A0 2C D0 C9 6F 8D E5 D7 EF 64 47 B3 2F 5E D1 F7 A5 51 B2 E7 66 14 5C 9B 94 6B 47 C4 B1 D1 7E 25 01 70 9B E6 F9 02 B6 84 30 42 4C 9A 10 D9 95 FE F7 D1 60 05 9B CB 58 BE 92 A3 BD 7E B8 D2 CA E3 2D E3 EF 80 E5 86 64 BC BD BC 28 6D B8 C4 37 FB BD C3 5C BD 67 FA 22 98 79 22 5C 99 E2 90 02 FB 3F 09 10 A5 71 A3 95 17 AC 14 8F 6F 48 8F 39 B4 4E 23 78 2D FF FF 77 D9 E8 B9 F6 5D E0 39 44 92 C7 56 E9 25 F4 52 1C 6A CE 4C A2 BF 44 94 97 D4 42 3E 71 FC 59 68 16 67 AB DC 7A 4A 96 1E F1 F3 3E 85 F2 4C 2B 1E 2A 78 04 73 28 60 87 4B D9 4D 62 05 3C 33 DA 0B 13 90 EC 62 AF 6A 15 30 D2 46 1D 45 EA 4B 8A 23 7A D6 8B C5 89 6C 8E CE 6E 84 03 82 2D B5 19 E6 20 93 01 C2 24 84 74 23 79 F1 F4 99 FD 7C CC 0A B4 B4 18 5D 34 09 9F EE CF 6A 89 33 B3 DF D2 00 AC 4F 3E D9 A6 99 C8 13 A5 6B C8 2C 3C 30 B0 FD B7 13 CB FB C5 21 3B 1A EB 90 FA C1 4F 08 99 04 5D C5 8A 72 42 30 A4 49 88 5B 1C 31 14 1F 12 A6 8A 52 E9 1C 09 16 86 A2 81 19 30 50 9F A9 66 31 1B 6D 97 31 01 DF A9 77 F5 EB 59 AE 37 90 02 63 FE 85 25 79 3D A1 C2 30 12 83 50 3F 77 CA 2A 28 3C 87 84 C4 BE 4A D1 8E A4 A8 89 DD F7 21 03 9E 6C D3 6E AA 0A 25 27 8D 66 E3 C3 7E 17 95 0B CE E7 2C 37 55 80 85 CA 61 83 94 E3 63 0A 4F B0 CB C5 42 CE 0F 00 3D 4B 77 49 A7 96 22 E2 E0 AF 43 6B CE F2 D5 5F 0D F6 4F DB 37 D1 EE 8D 69 97 92 CD 2A BC 45 9D 3B 47 27 37 6B E3 DF 4B D8 FB A2 67 8F 7C 19 95 EE 98 E9 3A 05 28 8D C9 26 56 CB E0 4E A8 99 F9 CD 80 6B 65 18 B2 AD 66 E2 1A 14 7A 03 2D 24 E3 90 12 6A 25 05 2D 5C 32 7A 1C 13 D4 B6 F5 6D 4E A2 5D 71 32 88 50 1D 0F B6 DD 61 23 F7 DC BB 5C 27 B6 EB 7F 5B 89 5D 6E 8A 19 CF 36 D2 7F E6 55 0F E5 18 F9 9F 49 6D AC E8 5D 8C 2A 6B B7 D8 6F 00 ED 2A F0 B0 5F DE 96 D4 3D 23 C9 B1 75 EE 91 EC B6 31 DF 50 2C F9 C8 BD FD F7 A6 BC 98 02 BA 9C 23 24 6C A4 3E D6 2A 5A 65 4C 39 72 0C 8E 43 29 08 4C 21 B1 A2 29 76 6F 19 03 6A 2C 8D DB 9F 2D A5 6F 01 34 A6 6C 80 3D A1 2E 90 FA D5 56 A3 89 C0 B8 09 04 31 60 73 A5 7C 8F 04 92 7E A3 7F 60 24 B7 69 E8 5C 5B 53 55 D9 03 B6 32 6E 0B BB 2D 57 AC C5 7E 70 58 C3 4A CE 35 E9 26 AB F6 B4 B7 AE 5D 03 82 8B 30 D8 9D 30 9B 93 4C B0 48 D2 D6 3B 04 95 20 18 14 EC 75 58 E8 42 DE 1E C7 B9 3F BF 86 E9 3C 82 D8 62 0F 97 4D D1 D8 7D EE B1 1C C0 15 17 A8 D4 A9 D2 5B 2F 12 95 C2 7D E8 4C 51 6D 95 88 C8 33 7E 69 24 8F E4 2F 7F 22 20 32 BD 01 C8 71 C6 C1 A8 3C CA BC 8F 64 37 5A 4A 15 A6 36 F5 31 34 7E 70 78 5B 1C 8F 0B 14 28 9B 43 D1 49 3B F7 91 DC 12 F5 EF C6 0D 39 FE B4 6B 4D E1 BA 87 96 CB 75 87 B2 78 A1 D7 F6 7D DA ED 77 FE 42 DE 28 29 DF 1A 1F 11 CA 70 BC AF 9D 7D 5E 07 23 C8 8B 2C F7 1A 40 A2 15 73 2B 92 DE 98 57 E7 3D 43 A2 2A 8C 66 6A 0C 9D 15 A4 65 17 F5 A5 15 F7 CF 4B AE D0 46 E6 4E 62 E2 6E D9 FB 64 17 A8 96 57 8A 8B E8 89 C3 73 A8 FB 49 92 F6 4A 51 0A BF 2E 33 2E F6 6D BD C0 C7 0C 4F 69 8D 10 BB 46 C8 B4 88 0C D8 ED 2E 2B 0D 1E 77 E3 C6 52 E6 6D 56 80 5E A6 EA 34 86 5D 3B 3D EE 19 F4 62 9C BF 7F B1 6E 78 A1 75 58 EB 94 DB B9 1F 9A D9 72 EB 1E 60 58 87 46 B3 9D 2B 2B D7 04 8A 13 30 CD 35 16 C2 0E AB C3 93 B7 F4 F8 25 C5 BA 20 B5 62 64 A6 78 D7 E9 FD AD BC D0 69 F9 A7 AF C1 9D B7 8A B2 DA A6 CF F4 5E F4 FB 20 52 2A F2 3D 2F C1 CD A5 70 3A 8F 1C 4D 93 77 DC 35 1E 77 9F B9 15 7B 84 0A 7B 69 86 20 CC BA 7F 60 6D F8 D4 0E 97 9A 90 62 ED 9F BE FC 65 C6 A1 BD 58 D2 64 3A E8 73 69 37 B7 33 80 6F 5F D3 6B 7A 24 A5 62 A0 23 0A B9 51 7F AC E6 A0 E3 95 DA 61 97""" + + """ 31 9D 14 F5 35 51 07 F7 6B 8E 3E 0B 5D C2 B9 C1 70 16 9E D2 2D 75 62 5E 62 82 CB 8B 22 A9 E9 DA 91 37 97 3B F9 95 AB 12 72 D1 7E 8C 87 2C A8 D1 AC CD 40 7B B1 5B 3E 4F 29 CA A6 D2 D8 9C 38 15 42 E6 66 35 9E C5 41 2C 4D BC 22 1F A1 AD 24 CE 4C C1 B2 E2 E0 A4 A4 DF E7 01 8B 10 90 C3 38 02 A9 0F 49 AA 8B 86 EC 58 B2 AA 79 6E FF 1E 1F 2A 35 3F 3D 96 A7 DC D0 F6 E5 18 4E EA C8 C5 72 76 3E 74 2E 12 05 78 6F FC BD 07 50 CA AF EE C2 11""", + received[1].toUHexString() + ) + + assertEquals( + """00 00 00 0B 01 00 00 00 00 0E 11 11 11 11 11 11 11 11 11 11 7C CC CC 8A 82 A1 2F A3 F6 08 8D 5C 7B 64 F8 BB 4F 09 87 B1 7C E3 01 53 5F C0 32 A8 6E A4 0A 1F 49 38 E5 FD 7F 20 52 A3 1E 66 A5 43 85 C4 96 97 3C 8F 2A 33 98 D7 A6 90 50 37 BE 76 2D 94 CE 1F D8 63 07 DC D1 5A 6F F0 FA 1F 41 9E 74 BE 8D FC 61 2C 66 3F BB 4C FB 9A 02 53 FF BE E5 FD 52 B7 FD CE DB 80 C0 6A 55 14 31 31 0C 8A 7D 24 DC EC 8C 45 62 ED D9 F6 DA BB EA C5 6E 76 66 11 F4 CB 2B 3B C7 45 6C BE 6F ED 9E 1D 2B 8A ED 3B C1 5B 06 DC 75 58 C9 66 13 4C 7D EA C5 F9 A4 D1 37 EF 2B BE 98 E3 37 1C 30 17 20 80 98 AD 5F BC 85 98 5B A1 7F 9E AE 54 0C 36 B0 6F FA 4A 5B 6F DC 7E B1 9E 99 F6 07 16 43 11 7A BC 82 F7 FB B2 95 7C 29 37 AF 38 70 6E A8 75 12 84 73 CF 15 A3 65 B9 2B E5 0A 01 30 67 A2 D2 BF 9F 30 AF 4D A5 A6 EA 4A B3 E4 BB AC 15 E3 FB CD FC B0 52 69 05 8D BE A0 11 1D A7 AB 65 6D F1 68 06 71 89 4F 0A 7E F4 68 A0 71 21 07 E4 85 41 76 16 07 78 2E C5 30 A6 2F 5D C7 DB EF 24 61 20 E6 6D FF 01 28 0D D6 73 42 BF F1 65 EA 7E DE 81 94 89 EB D0 65 8D 94 39 BE 89 7F B0 B3 0C 74 2F FA B4 2B 86 22 08 67 EB FA 62 83 FA BD CA 9D 92 95 A6 B0 BB 0C 76 07 C4 DC B9 E0 CD 67 AB 84 4D CC 55 52 E5 22 CD FD AC 08 35 81 36 9A 2D 7B 56 66 E8 B4 D9 A5 AA 7A 57 F2 F6 FB 20 A1 78 D5 5D F3 7C E4 7F 34 53 59 52 38 06 35 AD 6D 7A 97 99 AA 97 1D 9D 4A E4 22 7A 11 0C EA 16 27 7C 8A 9E F2 5A 82 D5 23 C3 01 BF D7 43 99 E7 26 25 AA 5A 27 29 ED 84 7F 9D 46 47 F8 C3 94 94 D7 C8 27 9F F7 93 16 4E 57 62 FC AC 32 2E A8 9F B2 62 EA 45 B0 53 A2 6C 52 D2 76 5F 48 00 93 5B 95 24 1F 3C E7 EC 40 26 66 0F C7 7D 30 91 A5 F2 6A 2F 2D CB 24 0B A3 F8 71 23 CA 31 A1 42 17 AC 9B 25 5A 2E 46 2D BC 34 DE 70 DA EF 4C E1 2D AE 2A 88 66 19 EE 35 02 C6 68 2C C4 0A 90 82 CA AB 42 D6 57 01 62 22 0D DF 65 6A 7E 6E 68 07 1D E6 1E DB E3 E6 22 F2 9A 36 F4 25 06 FA 91 83 9C 98 EC FB FD F0 F9 62 97 E6 37 30 F3 B3 09 46 D9 7D 7F D5 51 FF 8A FA B7 33 C9 15 AC D3 93 B7 8F 36 E8 1A 85 42 E5 C5 00 E6 28 9C FA 6C""", + received[2].toUHexString() + ) + } + + private inline fun buildLVPacket(block: BytePacketBuilder.() -> Unit): ByteReadPacket { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + return buildPacket { + writeIntLVPacket(lengthOffset = { it + 4 }) { + block() + } + } + } + +} \ No newline at end of file diff --git a/mirai-core/src/nativeTest/kotlin/network/framework/AbstractCommonNHTest.kt b/mirai-core/src/nativeTest/kotlin/network/framework/AbstractCommonNHTest.kt index 67a3095c7..57637ddaa 100644 --- a/mirai-core/src/nativeTest/kotlin/network/framework/AbstractCommonNHTest.kt +++ b/mirai-core/src/nativeTest/kotlin/network/framework/AbstractCommonNHTest.kt @@ -19,16 +19,24 @@ import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory internal actual abstract class AbstractCommonNHTest actual constructor() : AbstractRealNetworkHandlerTest<TestCommonNetworkHandler>() { - actual override val network: TestCommonNetworkHandler - get() = TODO("Not yet implemented") - actual override val factory: NetworkHandlerFactory<TestCommonNetworkHandler> - get() = TODO("Not yet implemented") + actual override val network: TestCommonNetworkHandler by lazy { + factory.create(createContext(), createAddress()) + } + + actual override val factory: NetworkHandlerFactory<TestCommonNetworkHandler> = + NetworkHandlerFactory<TestCommonNetworkHandler> { context, address -> + object : TestCommonNetworkHandler(bot, context, address) { + override suspend fun createConnection(): PlatformConn { + return conn + } + } + } + protected actual fun removeOutgoingPacketEncoder() { } - actual val conn: PlatformConn - get() = TODO("Not yet implemented") + actual val conn: PlatformConn = PlatformConn() } diff --git a/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt b/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt index 8f967cd69..a726b28fd 100644 --- a/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt +++ b/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt @@ -10,11 +10,13 @@ package net.mamoe.mirai.internal.test import net.mamoe.mirai.IMirai +import net.mamoe.mirai.internal.initMirai import net.mamoe.mirai.utils.setSystemProp import kotlin.test.Test internal actual fun initPlatform() { + initMirai() } internal actual class PlatformInitializationTest actual constructor() : AbstractTest() { @@ -25,8 +27,14 @@ internal actual class PlatformInitializationTest actual constructor() : Abstract /** * All test classes should inherit from [AbstractTest] + * + * Note: To run a test in native sourceSets, use IDEA key shortcut 'control + shift + R' on macOS and 'Ctrl + Shift + R' on Windows. + * Or you can right-click the function name of the test case and invoke 'Run ...'. You should not expect to see a button icon around the line numbers. */ internal actual abstract class AbstractTest actual constructor() : CommonAbstractTest() { + init { + Companion + } actual companion object { init { diff --git a/mirai-core/src/unixMain/cinterop/Socket.def b/mirai-core/src/unixMain/cinterop/Socket.def new file mode 100644 index 000000000..777694743 --- /dev/null +++ b/mirai-core/src/unixMain/cinterop/Socket.def @@ -0,0 +1,30 @@ +headers = netdb.h + +--- + +#include <stdlib.h> +#include <string.h> +#include <netdb.h> + +static int socket_create_connect(char *host, ushort port) { + struct hostent *he; + struct sockaddr_in their_addr; /* connector's address information */ + if ((he = gethostbyname(host)) == NULL) { /* get the host info */ + return -1; + } + int sockfd; + if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + return -2; + } + + their_addr.sin_family = AF_INET; /* host byte order */ + their_addr.sin_port = htons(port); /* short, network byte order */ + their_addr.sin_addr = *((struct in_addr *) he->h_addr); + bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */ + + if (connect(sockfd, (struct sockaddr *) &their_addr, sizeof(struct sockaddr)) == -1) { + return -3; + } + + return sockfd; +} \ No newline at end of file diff --git a/mirai-core/src/unixMain/kotlin/package.kt b/mirai-core/src/unixMain/kotlin/package.kt new file mode 100644 index 000000000..7df5cebc4 --- /dev/null +++ b/mirai-core/src/unixMain/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal \ No newline at end of file diff --git a/mirai-core/src/unixMain/kotlin/utils/PlatformSocket.kt b/mirai-core/src/unixMain/kotlin/utils/PlatformSocket.kt new file mode 100644 index 000000000..8cd91578c --- /dev/null +++ b/mirai-core/src/unixMain/kotlin/utils/PlatformSocket.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.EOFException +import io.ktor.utils.io.errors.* +import kotlinx.cinterop.* +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel +import net.mamoe.mirai.utils.* +import platform.posix.close +import platform.posix.errno +import platform.posix.recv +import platform.posix.write +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * TCP Socket. + */ +internal actual class PlatformSocket( + private val socket: Int, + bufferSize: Int = DEFAULT_BUFFER_SIZE * 2 // improve performance for some big packets +) : Closeable, HighwayProtocolChannel { + @Suppress("UnnecessaryOptInAnnotation") + @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) + private val readDispatcher: CoroutineDispatcher = newSingleThreadContext("PlatformSocket#$socket.dispatcher") + + // Native send and read are blocking. Using a dedicated thread(dispatcher) to do the job. + @Suppress("UnnecessaryOptInAnnotation") + @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) + private val sendDispatcher: CoroutineDispatcher = newSingleThreadContext("PlatformSocket#$socket.dispatcher") + + private val readLock = Mutex() + private val writeLock = Mutex() + private val writeBuffer = ByteArray(bufferSize).pin() + + actual val isOpen: Boolean + get() = write(socket, null, 0).convert<Long>() != 0L + + actual override fun close() { + close(socket) + (readDispatcher as CloseableCoroutineDispatcher).close() + (sendDispatcher as CloseableCoroutineDispatcher).close() + writeBuffer.unpin() + } + + @OptIn(ExperimentalIoApi::class) + actual suspend fun send(packet: ByteArray, offset: Int, length: Int): Unit = writeLock.withLock { + withContext(sendDispatcher) { + require(offset >= 0) { "offset must >= 0" } + require(length >= 0) { "length must >= 0" } + require(offset + length <= packet.size) { "It must follows offset + length <= packet.size" } + packet.usePinned { pin -> + if (write(socket, pin.addressOf(offset), length.convert()).convert<Long>() < 0L) { + throw PosixException.forErrno(posixFunctionName = "write()").wrapIO() + } + } + } + } + + /** + * @throws SendPacketInternalException + */ + @OptIn(ExperimentalIoApi::class) + actual override suspend fun send(packet: ByteReadPacket): Unit = writeLock.withLock { + withContext(sendDispatcher) { + logger.info { "Native socket sending: len=${packet.remaining}" } + val writeBuffer = writeBuffer + while (packet.remaining != 0L) { + val length = packet.readAvailable(writeBuffer.get()) + if (write(socket, writeBuffer.addressOf(0), length.convert()).convert<Long>() < 0L) { + throw PosixException.forErrno(posixFunctionName = "write()").wrapIO() + } + logger.info { "Native socket sent $length bytes." } + } + } + } + + /** + * @throws ReadPacketInternalException + */ + actual override suspend fun read(): ByteReadPacket = readLock.withLock { + withContext(readDispatcher) { + logger.info { "Native socket reading." } + + val readBuffer = ByteArrayPool.borrow() + + try { + val length = readBuffer.usePinned { pinned -> + recv(socket, pinned.addressOf(0), pinned.get().size.convert(), 0).convert<Long>() + } + + if (length <= 0L) throw EOFException("recv: $length, errno=$errno") + logger.info { + "Native socket read $length bytes: ${ + readBuffer.copyOf(length.toInt()).toUHexString() + }" + } + readBuffer.toReadPacket(length = length.toInt()) { ByteArrayPool.recycle(it) } + } catch (e: Throwable) { + ByteArrayPool.recycle(readBuffer) + throw e + } + } + } + + actual companion object { + private val logger: MiraiLogger = MiraiLogger.Factory.create(PlatformSocket::class) + + actual suspend fun connect( + serverIp: String, + serverPort: Int + ): PlatformSocket { + val r = sockets.socket_create_connect(serverIp.cstr, serverPort.toUShort()) + if (r < 0) error("Failed socket_create_connect: $r") + return PlatformSocket(r) +// val addr = nativeHeap.alloc<sockaddr_in>() { +// sin_family = AF_INET.toUByte() +// sin_addr.s_addr = resolveIpFromHost(serverIp).pointed.s_addr +// sin_port = serverPort.toUInt().toUShort() +// } +// +// val id = socket(AF_INET, SOCK_STREAM, 0) +// if (id == -1) throw PosixException.forErrno(posixFunctionName = "socket()") +// +// println("connect") +// val conn = connect(id, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().toUInt()) +// println("connect: $conn, $errno") +// if (conn < 0) throw PosixException.forErrno(posixFunctionName = "connect()") +// +// return PlatformSocket(conn) + } + +// private fun resolveIpFromHost(serverIp: String): CPointer<in_addr> { +// val host = gethostbyname(serverIp) // points to static data, don't free +// ?: throw IllegalStateException("Failed to resolve IP from host. host=$serverIp") +// println(host.pointed.h_addr_list?.get(1)?.reinterpret<in_addr>()?.pointed?.s_addr) +// return host.pointed.h_addr_list?.get(1)?.reinterpret<in_addr>() ?: error("Failed to get ip") +//// val hAddrList = host.pointed.h_addr_list +//// ?: throw IllegalStateException("Empty IP list resolved from host. host=$serverIp") +//// +//// +//// val str = hAddrList[0]!!.reinterpret<UIntVar>() +//// +//// try { +//// return str.pointed.value +//// } finally { +////// free() +//// } +// } + + actual suspend inline fun <R> withConnection( + serverIp: String, + serverPort: Int, + block: PlatformSocket.() -> R + ): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return connect(serverIp, serverPort).use(block) + } + } + +}