mirror of
https://github.com/mamoe/mirai.git
synced 2025-04-25 04:50:26 +08:00
Multiplatform MiraiFile and BinaryOps implementations
This commit is contained in:
parent
11a2354de4
commit
8d65a22f01
mirai-core-api/src
mirai-core-utils
build.gradle.kts
src
commonMain/kotlin
jvmBaseMain/kotlin
mingwMain/kotlin
mingwTest/kotlin
nativeMain/kotlin
nativeTest/kotlin
unixMain/kotlin
unixTest/kotlin
mirai-core/src/commonMain/kotlin/network
@ -154,6 +154,7 @@ public interface CancellableEvent : Event {
|
||||
* [EventChannel.filter] 和 [Listener.onEvent] 时产生的异常只会由监听方处理.
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
@Suppress("TOP_LEVEL_FUNCTIONS_NOT_SUPPORTED") // compiler bug
|
||||
public suspend fun <E : Event> E.broadcast(): E {
|
||||
Mirai.broadcastEvent(this)
|
||||
return this
|
||||
|
@ -139,7 +139,7 @@ public expect open class BotConfiguration() { // open for Java
|
||||
* 心跳策略.
|
||||
* @since 2.6.3
|
||||
*/
|
||||
public enum class HeartbeatStrategy {
|
||||
public enum class HeartbeatStrategy { // IN ACTUAL DECLARATION DO NOT ADD EXTRA ELEMENTS.
|
||||
/**
|
||||
* 使用 2.6.0 增加的*状态心跳* (Stat Heartbeat). 通常推荐这个模式.
|
||||
*
|
||||
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.IMirai
|
||||
import net.mamoe.mirai.event.ConcurrencyKind.CONCURRENT
|
||||
import net.mamoe.mirai.event.ConcurrencyKind.LOCKED
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -107,7 +108,8 @@ public actual abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
|
||||
coroutineContext: CoroutineContext,
|
||||
priority: EventPriority,
|
||||
): Listener<@UnsafeVariance BaseEvent> {
|
||||
return subscribe(baseEventClass, coroutineContext, priority = priority) {
|
||||
// keep this LOCKED, otherwise compiler will choose the 'inline subscribe' which takes no KClass arg.
|
||||
return subscribe(baseEventClass, coroutineContext, LOCKED, priority) {
|
||||
try {
|
||||
channel.send(it)
|
||||
ListeningStatus.LISTENING
|
||||
|
@ -72,6 +72,17 @@ kotlin {
|
||||
|
||||
val nativeMain by getting {
|
||||
dependencies {
|
||||
// implementation("com.soywiz.korlibs.krypto:krypto:2.4.12") // ':mirai-core-utils:compileNativeMainKotlinMetadata' fails because compiler cannot find reference
|
||||
}
|
||||
}
|
||||
|
||||
val mingwMain by getting {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
val unixMain by getting {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,24 @@ import io.ktor.utils.io.core.*
|
||||
* Multiplatform implementation of file operations.
|
||||
*/
|
||||
public expect interface MiraiFile {
|
||||
/**
|
||||
* Name of this file or directory. Can be '.' and '..' if created by
|
||||
*/
|
||||
public val name: String
|
||||
|
||||
/**
|
||||
* Parent of this file or directory.
|
||||
*/
|
||||
public val parent: MiraiFile?
|
||||
|
||||
/**
|
||||
* Input path from [create].
|
||||
*/
|
||||
public val path: String
|
||||
|
||||
/**
|
||||
* Normalized absolute [path].
|
||||
*/
|
||||
public val absolutePath: String
|
||||
|
||||
public val length: Long
|
||||
@ -27,6 +42,9 @@ public expect interface MiraiFile {
|
||||
|
||||
public fun exists(): Boolean
|
||||
|
||||
/**
|
||||
* Resolves a [MiraiFile] representing the [path] based on this [MiraiFile]. Result path is not guaranteed to be normalized.
|
||||
*/
|
||||
public fun resolve(path: String): MiraiFile
|
||||
public fun resolve(file: MiraiFile): MiraiFile
|
||||
|
||||
@ -40,11 +58,13 @@ public expect interface MiraiFile {
|
||||
public fun output(): Output
|
||||
|
||||
public companion object {
|
||||
public fun create(absolutePath: String): MiraiFile
|
||||
public fun create(path: String): MiraiFile
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public expect fun MiraiFile.deleteRecursively(): Boolean
|
||||
|
||||
public fun MiraiFile.writeBytes(data: ByteArray) {
|
||||
return output().use { it.writeFully(data) }
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF)
|
||||
public fun Long.toLongUnsigned(): Long = this // for native unstable types
|
||||
public fun Short.toIntUnsigned(): Int = this.toUShort().toInt()
|
||||
public fun Byte.toIntUnsigned(): Int = toInt() and 0xFF
|
||||
public fun Int.concatAsLong(i2: Int): Long = this.toLongUnsigned().shl(Int.SIZE_BITS) or i2.toLongUnsigned()
|
||||
|
@ -14,9 +14,24 @@ import io.ktor.utils.io.streams.*
|
||||
import java.io.File
|
||||
|
||||
public actual interface MiraiFile {
|
||||
/**
|
||||
* Name of this file or directory. Can be '.' and '..' if created by
|
||||
*/
|
||||
public actual val name: String
|
||||
|
||||
/**
|
||||
* Parent of this file or directory.
|
||||
*/
|
||||
public actual val parent: MiraiFile?
|
||||
|
||||
/**
|
||||
* Input path from [create].
|
||||
*/
|
||||
public actual val path: String
|
||||
|
||||
/**
|
||||
* Normalized absolute [path].
|
||||
*/
|
||||
public actual val absolutePath: String
|
||||
|
||||
public actual val length: Long
|
||||
@ -26,6 +41,9 @@ public actual interface MiraiFile {
|
||||
|
||||
public actual fun exists(): Boolean
|
||||
|
||||
/**
|
||||
* Resolves a [MiraiFile] representing the [path] based on this [MiraiFile]. Result path is not guaranteed to be normalized.
|
||||
*/
|
||||
public actual fun resolve(path: String): MiraiFile
|
||||
public actual fun resolve(file: MiraiFile): MiraiFile
|
||||
|
||||
@ -39,12 +57,16 @@ public actual interface MiraiFile {
|
||||
public actual fun output(): Output
|
||||
|
||||
public actual companion object {
|
||||
public actual fun create(absolutePath: String): MiraiFile {
|
||||
return File(absolutePath).asMiraiFile()
|
||||
public actual fun create(path: String): MiraiFile {
|
||||
return File(path).asMiraiFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public actual fun MiraiFile.deleteRecursively(): Boolean {
|
||||
return this.toJvmFile().deleteRecursively()
|
||||
}
|
||||
|
||||
public fun File.asMiraiFile(): MiraiFile {
|
||||
return JvmFileAsMiraiFile(this)
|
||||
}
|
||||
@ -61,6 +83,7 @@ internal class JvmFileAsMiraiFile(
|
||||
) : MiraiFile {
|
||||
override val name: String get() = jvmFile.name
|
||||
override val parent: MiraiFile? get() = jvmFile.parentFile?.asMiraiFile()
|
||||
override val path: String get() = jvmFile.path
|
||||
override val absolutePath: String get() = jvmFile.absolutePath
|
||||
override val length: Long get() = jvmFile.length()
|
||||
override val isFile: Boolean get() = jvmFile.isFile
|
||||
|
@ -17,32 +17,34 @@ import platform.windows.*
|
||||
|
||||
internal actual class MiraiFileImpl actual constructor(
|
||||
// canonical
|
||||
absolutePath: String
|
||||
path: String
|
||||
) : MiraiFile {
|
||||
override val path = path.replace("/", "\\")
|
||||
|
||||
companion object {
|
||||
private val ROOT_REGEX = Regex("""^([a-zA-z]+:[/\\])""")
|
||||
private const val SEPARATOR = "\\"
|
||||
private const val SEPARATOR = '\\'
|
||||
}
|
||||
|
||||
override val absolutePath: String = kotlin.run {
|
||||
val result = ROOT_REGEX.matchEntire(absolutePath) ?: return@run absolutePath.dropLastWhile { it.isSeparator() }
|
||||
val result = ROOT_REGEX.matchEntire(path) ?: return@run path.dropLastWhile { it.isSeparator() }
|
||||
return@run result.groups.first()!!.value
|
||||
}
|
||||
|
||||
private fun Char.isSeparator() = this == '/' || this == '\\'
|
||||
|
||||
// TODO: 2022/5/28 normalize paths
|
||||
override val parent: MiraiFile? by lazy {
|
||||
val p = absolutePath.substringBeforeLast('/')
|
||||
val absolute = absolutePath
|
||||
val p = absolute.substringBeforeLast(SEPARATOR, "")
|
||||
if (p.isEmpty()) {
|
||||
return@lazy null
|
||||
}
|
||||
if (p.lastOrNull() == ':') {
|
||||
if (p.lastIndexOf('/') == p.lastIndex) {
|
||||
// C:/
|
||||
if (absolute.lastIndexOf(SEPARATOR) == p.lastIndex) {
|
||||
// file is C:/
|
||||
return@lazy null
|
||||
} else {
|
||||
return@lazy MiraiFileImpl("$p/") // C:/
|
||||
return@lazy MiraiFileImpl("$p/") // file is C:/xxx
|
||||
}
|
||||
}
|
||||
MiraiFileImpl(p)
|
||||
@ -86,19 +88,17 @@ internal actual class MiraiFileImpl actual constructor(
|
||||
|
||||
private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesA(absolutePath) }
|
||||
|
||||
// TODO: 2022/5/28 normalize paths
|
||||
override fun resolve(path: String): MiraiFile {
|
||||
when (path) {
|
||||
"." -> return this
|
||||
".." -> return parent ?: this // root
|
||||
}
|
||||
|
||||
if (path.matches(ROOT_REGEX)) {
|
||||
if (ROOT_REGEX.find(path) != null) { // absolute
|
||||
return MiraiFileImpl(path)
|
||||
}
|
||||
|
||||
val new = MiraiFileImpl(path)
|
||||
return MiraiFileImpl("$absolutePath${SEPARATOR}${new.parent}/${new.name}")
|
||||
return MiraiFileImpl(this.absolutePath + SEPARATOR + path) // assuming path is 'appendable'
|
||||
}
|
||||
|
||||
override fun resolve(file: MiraiFile): MiraiFile {
|
||||
|
14
mirai-core-utils/src/mingwMain/kotlin/StandardUtils.kt
Normal file
14
mirai-core-utils/src/mingwMain/kotlin/StandardUtils.kt
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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 platform.windows.GetCurrentProcessorNumber
|
||||
|
||||
public actual fun availableProcessors(): Int = GetCurrentProcessorNumber().toInt()
|
39
mirai-core-utils/src/mingwTest/kotlin/MiraiFileImplTest.kt
Normal file
39
mirai-core-utils/src/mingwTest/kotlin/MiraiFileImplTest.kt
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class WindowsMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
|
||||
private val rand = Random.nextInt().absoluteValue
|
||||
override val baseTempDir: MiraiFile = MiraiFile.create("C:/Users/Shared/mirai_test")
|
||||
override val tempPath = "C:/Users/Shared/mirai_test/temp$rand"
|
||||
private val tempDir = MiraiFile.create(tempPath).apply {
|
||||
assertTrue("Failed to make temp directory: ${this.absolutePath}") { mkdirs() }
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
@ -11,46 +11,131 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import interop.*
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.free
|
||||
import platform.posix.uint8_tVar
|
||||
import platform.zlib.*
|
||||
|
||||
|
||||
public actual val DEFAULT_BUFFER_SIZE: Int get() = 8192
|
||||
|
||||
public actual fun ByteArray.md5(offset: Int, length: Int): ByteArray = callImpl(::mirai_hash_md5, offset, length)
|
||||
public actual fun ByteArray.sha1(offset: Int, length: Int): ByteArray = callImpl(::mirai_hash_sh1, offset, length)
|
||||
public actual fun ByteArray.md5(offset: Int, length: Int): ByteArray {
|
||||
MD5.create().run {
|
||||
update(this@md5, offset, length)
|
||||
return digest().bytes
|
||||
}
|
||||
}
|
||||
|
||||
public actual fun ByteArray.gzip(offset: Int, length: Int): ByteArray =
|
||||
callImpl(::mirai_compression_gzip, offset, length)
|
||||
public actual fun ByteArray.sha1(offset: Int, length: Int): ByteArray = SHA1.create().run {
|
||||
update(this@sha1, offset, length)
|
||||
return digest().bytes
|
||||
}
|
||||
|
||||
public actual fun ByteArray.ungzip(offset: Int, length: Int): ByteArray =
|
||||
callImpl(::mirai_compression_ungzip, offset, length)
|
||||
public actual fun ByteArray.gzip(offset: Int, length: Int): ByteArray {
|
||||
val output = ByteArray(length * 5)
|
||||
output.usePinned { out ->
|
||||
usePinned { pin ->
|
||||
memScoped {
|
||||
val z = alloc<z_stream>()
|
||||
z.avail_in = size.toUInt()
|
||||
z.next_in = pin.addressOf(0).reinterpret()
|
||||
z.avail_out = output.size.toUInt()
|
||||
val initialOutAddress = out.addressOf(0)
|
||||
z.next_out = initialOutAddress.reinterpret()
|
||||
deflateInit2(z.ptr, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 or 16, 8, Z_DEFAULT_STRATEGY)
|
||||
deflate(z.ptr, Z_FINISH) // TODO: 2022/5/28 buf
|
||||
deflateEnd(z.ptr)
|
||||
|
||||
public actual fun ByteArray.deflate(offset: Int, length: Int): ByteArray =
|
||||
callImpl(::mirai_compression_deflate, offset, length)
|
||||
|
||||
public actual fun ByteArray.inflate(offset: Int, length: Int): ByteArray =
|
||||
callImpl(::mirai_compression_infalte, offset, length)
|
||||
|
||||
|
||||
private fun ByteArray.callImpl(
|
||||
fn: (CValuesRef<uint8_tVar>, UInt, CValuesRef<SizedByteArray>) -> Boolean,
|
||||
offset: Int,
|
||||
length: Int
|
||||
): ByteArray {
|
||||
checkOffsetAndLength(offset, length)
|
||||
|
||||
memScoped {
|
||||
val r = alloc<SizedByteArray>()
|
||||
if (!fn(toCValues().ptr.reinterpret<uint8_tVar>().plus(offset)!!, length.toUInt(), r.ptr)) {
|
||||
throw IllegalStateException("Failed platform implementation call")
|
||||
}
|
||||
try {
|
||||
return r.arr?.readBytes(r.size.toInt())!!
|
||||
} finally {
|
||||
free(r.arr)
|
||||
val resultSize = z.next_out.toLong() - initialOutAddress.toLong()
|
||||
return output.copyOf(resultSize.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 2022/5/28 optimize length
|
||||
|
||||
public actual fun ByteArray.ungzip(offset: Int, length: Int): ByteArray {
|
||||
val output = ByteArray(length)
|
||||
output.usePinned { out ->
|
||||
usePinned { pin ->
|
||||
memScoped {
|
||||
val z = alloc<z_stream>()
|
||||
z.avail_in = size.toUInt()
|
||||
z.next_in = pin.addressOf(0).reinterpret()
|
||||
z.avail_out = output.size.toUInt()
|
||||
val initialOutAddress = out.addressOf(0)
|
||||
z.next_out = initialOutAddress.reinterpret()
|
||||
inflateInit2(z.ptr, 15 or 16)
|
||||
inflate(z.ptr, Z_FINISH)
|
||||
inflateEnd(z.ptr)
|
||||
|
||||
val resultSize = z.next_out.toLong() - initialOutAddress.toLong()
|
||||
return output.copyOf(resultSize.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public actual fun ByteArray.deflate(offset: Int, length: Int): ByteArray {
|
||||
val output = ByteArray(length * 2)
|
||||
output.usePinned { out ->
|
||||
usePinned { pin ->
|
||||
memScoped {
|
||||
val z = alloc<z_stream>()
|
||||
z.avail_in = size.toUInt()
|
||||
z.next_in = pin.addressOf(0).reinterpret()
|
||||
z.avail_out = output.size.toUInt()
|
||||
val initialOutAddress = out.addressOf(0)
|
||||
z.next_out = initialOutAddress.reinterpret()
|
||||
deflateInit(z.ptr, Z_DEFAULT_COMPRESSION)
|
||||
deflate(z.ptr, Z_FINISH)
|
||||
deflateEnd(z.ptr)
|
||||
|
||||
val resultSize = z.next_out.toLong() - initialOutAddress.toLong()
|
||||
return output.copyOf(resultSize.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public actual fun ByteArray.inflate(offset: Int, length: Int): ByteArray {
|
||||
val output = ByteArray(length)
|
||||
output.usePinned { out ->
|
||||
usePinned { pin ->
|
||||
memScoped {
|
||||
val z = alloc<z_stream>()
|
||||
z.avail_in = size.toUInt()
|
||||
z.next_in = pin.addressOf(0).reinterpret()
|
||||
z.avail_out = output.size.toUInt()
|
||||
val initialOutAddress = out.addressOf(0)
|
||||
z.next_out = initialOutAddress.reinterpret()
|
||||
inflateInit(z.ptr)
|
||||
inflate(z.ptr, Z_FINISH)
|
||||
inflateEnd(z.ptr)
|
||||
|
||||
val resultSize = z.next_out.toLong() - initialOutAddress.toLong()
|
||||
return output.copyOf(resultSize.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//private fun ByteArray.callImpl(
|
||||
// fn: (CValuesRef<uint8_tVar>, UInt, CValuesRef<SizedByteArray>) -> Boolean,
|
||||
// offset: Int,
|
||||
// length: Int
|
||||
//): ByteArray {
|
||||
// checkOffsetAndLength(offset, length)
|
||||
//
|
||||
// memScoped {
|
||||
// val r = alloc<SizedByteArray>()
|
||||
// if (!fn(toCValues().ptr.reinterpret<uint8_tVar>().plus(offset)!!, length.toUInt(), r.ptr)) {
|
||||
// throw IllegalStateException("Failed platform implementation call")
|
||||
// }
|
||||
// try {
|
||||
// return r.arr?.readBytes(r.size.toInt())!!
|
||||
// } finally {
|
||||
// free(r.arr)
|
||||
// }
|
||||
// }
|
||||
//}
|
274
mirai-core-utils/src/nativeMain/kotlin/CommonDigest.kt
Normal file
274
mirai-core-utils/src/nativeMain/kotlin/CommonDigest.kt
Normal file
@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sin
|
||||
|
||||
/*
|
||||
* Note: All the declarations in this file are copied from 'com.soywiz.korlibs.krypto'. <https://github.com/korlibs/krypto>
|
||||
*
|
||||
* The license is attached:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Carlos Ballesteros Velasco
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
----------------------------------
|
||||
|
||||
/**
|
||||
* Based on CryptoJS v3.1.2
|
||||
* code.google.com/p/crypto-js
|
||||
* (c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||
* https://github.com/brix/crypto-js/blob/develop/LICENSE
|
||||
*/
|
||||
*/
|
||||
|
||||
internal inline fun Int.ext8(offset: Int) = (this ushr offset) and 0xFF
|
||||
|
||||
internal fun Int.rotateRight(amount: Int): Int = (this ushr amount) or (this shl (32 - amount))
|
||||
internal fun Int.rotateLeft(bits: Int): Int = ((this shl bits) or (this ushr (32 - bits)))
|
||||
|
||||
internal fun arraycopy(src: ByteArray, srcPos: Int, dst: ByteArray, dstPos: Int, count: Int) =
|
||||
src.copyInto(dst, dstPos, srcPos, srcPos + count)
|
||||
|
||||
internal fun arraycopy(src: IntArray, srcPos: Int, dst: IntArray, dstPos: Int, count: Int) =
|
||||
src.copyInto(dst, dstPos, srcPos, srcPos + count)
|
||||
|
||||
internal fun ByteArray.readU8(o: Int): Int = this[o].toInt() and 0xFF
|
||||
internal fun ByteArray.readS32_be(o: Int): Int =
|
||||
(readU8(o + 3) shl 0) or (readU8(o + 2) shl 8) or (readU8(o + 1) shl 16) or (readU8(o + 0) shl 24)
|
||||
|
||||
internal abstract class Hasher(val chunkSize: Int, val digestSize: Int) {
|
||||
private val chunk = ByteArray(chunkSize)
|
||||
private var writtenInChunk = 0
|
||||
private var totalWritten = 0L
|
||||
|
||||
fun reset(): Hasher {
|
||||
coreReset()
|
||||
writtenInChunk = 0
|
||||
totalWritten = 0L
|
||||
return this
|
||||
}
|
||||
|
||||
fun update(data: ByteArray, offset: Int, count: Int): Hasher {
|
||||
var curr = offset
|
||||
var left = count
|
||||
while (left > 0) {
|
||||
val remainingInChunk = chunkSize - writtenInChunk
|
||||
val toRead = min(remainingInChunk, left)
|
||||
arraycopy(data, curr, chunk, writtenInChunk, toRead)
|
||||
left -= toRead
|
||||
curr += toRead
|
||||
writtenInChunk += toRead
|
||||
if (writtenInChunk >= chunkSize) {
|
||||
writtenInChunk -= chunkSize
|
||||
coreUpdate(chunk)
|
||||
}
|
||||
}
|
||||
totalWritten += count
|
||||
return this
|
||||
}
|
||||
|
||||
fun digestOut(out: ByteArray) {
|
||||
val pad = corePadding(totalWritten)
|
||||
var padPos = 0
|
||||
while (padPos < pad.size) {
|
||||
val padSize = chunkSize - writtenInChunk
|
||||
arraycopy(pad, padPos, chunk, writtenInChunk, padSize)
|
||||
coreUpdate(chunk)
|
||||
writtenInChunk = 0
|
||||
padPos += padSize
|
||||
}
|
||||
|
||||
coreDigest(out)
|
||||
coreReset()
|
||||
}
|
||||
|
||||
protected abstract fun coreReset()
|
||||
protected abstract fun corePadding(totalWritten: Long): ByteArray
|
||||
protected abstract fun coreUpdate(chunk: ByteArray)
|
||||
protected abstract fun coreDigest(out: ByteArray)
|
||||
|
||||
fun update(data: ByteArray) = update(data, 0, data.size)
|
||||
fun digest(): Hash = Hash(ByteArray(digestSize).also { digestOut(it) })
|
||||
}
|
||||
|
||||
internal value class Hash(val bytes: ByteArray)
|
||||
|
||||
internal open class HasherFactory(val create: () -> Hasher) {
|
||||
fun digest(data: ByteArray) = create().also { it.update(data, 0, data.size) }.digest()
|
||||
|
||||
inline fun digest(temp: ByteArray = ByteArray(0x1000), readBytes: (data: ByteArray) -> Int): Hash =
|
||||
this.create().also {
|
||||
while (true) {
|
||||
val count = readBytes(temp)
|
||||
if (count <= 0) break
|
||||
it.update(temp, 0, count)
|
||||
}
|
||||
}.digest()
|
||||
}
|
||||
|
||||
internal class MD5 : Hasher(chunkSize = 64, digestSize = 16) {
|
||||
companion object : HasherFactory({ MD5() }) {
|
||||
private val S = intArrayOf(7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21)
|
||||
private val T = IntArray(64) { ((1L shl 32) * abs(sin(1.0 + it))).toLong().toInt() }
|
||||
}
|
||||
|
||||
private val r = IntArray(4)
|
||||
private val o = IntArray(4)
|
||||
private val b = IntArray(16)
|
||||
|
||||
init {
|
||||
coreReset()
|
||||
}
|
||||
|
||||
override fun coreReset() {
|
||||
r[0] = 0x67452301
|
||||
r[1] = 0xEFCDAB89.toInt()
|
||||
r[2] = 0x98BADCFE.toInt()
|
||||
r[3] = 0x10325476
|
||||
}
|
||||
|
||||
override fun coreUpdate(chunk: ByteArray) {
|
||||
for (j in 0 until 64) b[j ushr 2] = (chunk[j].toInt() shl 24) or (b[j ushr 2] ushr 8)
|
||||
for (j in 0 until 4) o[j] = r[j]
|
||||
for (j in 0 until 64) {
|
||||
val d16 = j / 16
|
||||
val f = when (d16) {
|
||||
0 -> (r[1] and r[2]) or (r[1].inv() and r[3])
|
||||
1 -> (r[1] and r[3]) or (r[2] and r[3].inv())
|
||||
2 -> r[1] xor r[2] xor r[3]
|
||||
3 -> r[2] xor (r[1] or r[3].inv())
|
||||
else -> 0
|
||||
}
|
||||
val bi = when (d16) {
|
||||
0 -> j
|
||||
1 -> (j * 5 + 1) and 0x0F
|
||||
2 -> (j * 3 + 5) and 0x0F
|
||||
3 -> (j * 7) and 0x0F
|
||||
else -> 0
|
||||
}
|
||||
val temp = r[1] + (r[0] + f + b[bi] + T[j]).rotateLeft(S[(d16 shl 2) or (j and 3)])
|
||||
r[0] = r[3]
|
||||
r[3] = r[2]
|
||||
r[2] = r[1]
|
||||
r[1] = temp
|
||||
}
|
||||
for (j in 0 until 4) r[j] += o[j]
|
||||
}
|
||||
|
||||
override fun corePadding(totalWritten: Long): ByteArray {
|
||||
val numberOfBlocks = ((totalWritten + 8) / chunkSize) + 1
|
||||
val totalWrittenBits = totalWritten * 8
|
||||
return ByteArray(((numberOfBlocks * chunkSize) - totalWritten).toInt()).apply {
|
||||
this[0] = 0x80.toByte()
|
||||
for (i in 0 until 8) this[this.size - 8 + i] = (totalWrittenBits ushr (8 * i)).toByte()
|
||||
}
|
||||
}
|
||||
|
||||
override fun coreDigest(out: ByteArray) {
|
||||
for (it in 0 until 16) out[it] = (r[it / 4] ushr ((it % 4) * 8)).toByte()
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class SHA(chunkSize: Int, digestSize: Int) : Hasher(chunkSize, digestSize) {
|
||||
override fun corePadding(totalWritten: Long): ByteArray {
|
||||
val tail = totalWritten % 64
|
||||
val padding = (if (64 - tail >= 9) 64 - tail else 128 - tail)
|
||||
val pad = ByteArray(padding.toInt()).apply { this[0] = 0x80.toByte() }
|
||||
val bits = (totalWritten * 8)
|
||||
for (i in 0 until 8) pad[pad.size - 1 - i] = ((bits ushr (8 * i)) and 0xFF).toByte()
|
||||
return pad
|
||||
}
|
||||
}
|
||||
|
||||
internal class SHA1 : SHA(chunkSize = 64, digestSize = 20) {
|
||||
companion object : HasherFactory({ SHA1() }) {
|
||||
private val H = intArrayOf(
|
||||
0x67452301L.toInt(),
|
||||
0xEFCDAB89L.toInt(),
|
||||
0x98BADCFEL.toInt(),
|
||||
0x10325476L.toInt(),
|
||||
0xC3D2E1F0L.toInt()
|
||||
)
|
||||
|
||||
private const val K0020: Int = 0x5A827999L.toInt()
|
||||
private const val K2040: Int = 0x6ED9EBA1L.toInt()
|
||||
private const val K4060: Int = 0x8F1BBCDCL.toInt()
|
||||
private const val K6080: Int = 0xCA62C1D6L.toInt()
|
||||
}
|
||||
|
||||
private val w = IntArray(80)
|
||||
private val h = IntArray(5)
|
||||
|
||||
override fun coreReset(): Unit {
|
||||
arraycopy(H, 0, h, 0, 5)
|
||||
}
|
||||
|
||||
init {
|
||||
coreReset()
|
||||
}
|
||||
|
||||
override fun coreUpdate(chunk: ByteArray) {
|
||||
for (j in 0 until 16) w[j] = chunk.readS32_be(j * 4)
|
||||
for (j in 16 until 80) w[j] = (w[j - 3] xor w[j - 8] xor w[j - 14] xor w[j - 16]).rotateLeft(1)
|
||||
|
||||
var a = h[0]
|
||||
var b = h[1]
|
||||
var c = h[2]
|
||||
var d = h[3]
|
||||
var e = h[4]
|
||||
|
||||
for (j in 0 until 80) {
|
||||
val temp = a.rotateLeft(5) + e + w[j] + when (j / 20) {
|
||||
0 -> ((b and c) or ((b.inv()) and d)) + K0020
|
||||
1 -> (b xor c xor d) + K2040
|
||||
2 -> ((b and c) xor (b and d) xor (c and d)) + K4060
|
||||
else -> (b xor c xor d) + K6080
|
||||
}
|
||||
|
||||
e = d
|
||||
d = c
|
||||
c = b.rotateLeft(30)
|
||||
b = a
|
||||
a = temp
|
||||
}
|
||||
|
||||
h[0] += a
|
||||
h[1] += b
|
||||
h[2] += c
|
||||
h[3] += d
|
||||
h[4] += e
|
||||
}
|
||||
|
||||
override fun coreDigest(out: ByteArray) {
|
||||
for (n in out.indices) out[n] = (h[n / 4] ushr (24 - 8 * (n % 4))).toByte()
|
||||
}
|
||||
}
|
@ -15,26 +15,40 @@ import io.ktor.utils.io.bits.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.ktor.utils.io.errors.*
|
||||
import io.ktor.utils.io.streams.*
|
||||
import kotlinx.cinterop.CPointer
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.ptr
|
||||
import platform.posix.FILE
|
||||
import platform.posix.fclose
|
||||
import platform.posix.feof
|
||||
import platform.posix.stat
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.*
|
||||
|
||||
/**
|
||||
* Multiplatform implementation of file operations.
|
||||
*/
|
||||
public actual interface MiraiFile {
|
||||
/**
|
||||
* Name of this file or directory. Can be '.' and '..' if created by
|
||||
*/
|
||||
public actual val name: String
|
||||
|
||||
/**
|
||||
* Parent of this file or directory.
|
||||
*/
|
||||
public actual val parent: MiraiFile?
|
||||
|
||||
/**
|
||||
* Input path from [create].
|
||||
*/
|
||||
public actual val path: String
|
||||
|
||||
/**
|
||||
* Normalized absolute [path].
|
||||
*/
|
||||
public actual val absolutePath: String
|
||||
public actual val length: Long
|
||||
public actual val isFile: Boolean
|
||||
public actual val isDirectory: Boolean
|
||||
public actual fun exists(): Boolean
|
||||
|
||||
/**
|
||||
* Resolves a [MiraiFile] representing the [path] based on this [MiraiFile]. Result path is not guaranteed to be normalized.
|
||||
*/
|
||||
public actual fun resolve(path: String): MiraiFile
|
||||
public actual fun resolve(file: MiraiFile): MiraiFile
|
||||
public actual fun createNewFile(): Boolean
|
||||
@ -45,14 +59,26 @@ public actual interface MiraiFile {
|
||||
public actual fun output(): Output
|
||||
|
||||
public actual companion object {
|
||||
public actual fun create(absolutePath: String): MiraiFile = MiraiFileImpl(absolutePath)
|
||||
public actual fun create(path: String): MiraiFile = MiraiFileImpl(path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal expect class MiraiFileImpl(
|
||||
absolutePath: String,
|
||||
) : MiraiFile
|
||||
private val deleteFile =
|
||||
staticCFunction<CPointer<ByteVarOf<Byte>>?, CPointer<stat>?, Int, CPointer<FTW>?, Int> { pathPtr, _, _, _ ->
|
||||
val path = pathPtr!!.toKString()
|
||||
if (remove(path) < 0) {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
public actual fun MiraiFile.deleteRecursively(): Boolean {
|
||||
return nftw(absolutePath, deleteFile, 10, FTW_DEPTH or FTW_MOUNT or FTW_PHYS) >= 0
|
||||
}
|
||||
|
||||
internal expect class MiraiFileImpl(path: String) : MiraiFile
|
||||
|
||||
|
||||
/*
|
||||
@ -130,6 +156,7 @@ internal class FileNotFoundException(message: String, cause: Throwable? = null)
|
||||
IOException(message, cause)
|
||||
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
internal class PosixFileInstanceOutput(val file: CPointer<FILE>) : AbstractOutput() {
|
||||
private var closed = false
|
||||
@ -157,6 +184,7 @@ internal class PosixFileInstanceOutput(val file: CPointer<FILE>) : AbstractOutpu
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
internal class PosixInputForFile(val file: CPointer<FILE>) : AbstractInput() {
|
||||
private var closed = false
|
||||
|
@ -9,14 +9,9 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import platform.posix._SC_NPROCESSORS_ONLN
|
||||
import platform.posix.sysconf
|
||||
|
||||
|
||||
public actual fun localIpAddress(): String = "192.168.1.123"
|
||||
|
||||
public actual fun availableProcessors(): Int = sysconf(_SC_NPROCESSORS_ONLN).toInt()
|
||||
|
||||
internal actual fun isSameClassPlatform(object1: Any, object2: Any): Boolean {
|
||||
return object1::class == object2::class
|
||||
}
|
@ -13,13 +13,18 @@ package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.*
|
||||
import kotlin.system.getTimeMillis
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
@OptIn(UnsafeNumber::class)
|
||||
public actual fun currentTimeMillis(): Long {
|
||||
return getTimeMillis()
|
||||
// Do not use getTimeMillis from stdlib, it doesn't support iosSimulatorArm64
|
||||
memScoped {
|
||||
val timeT = alloc<time_tVar>()
|
||||
time(timeT.ptr)
|
||||
return timeT.value.toLongUnsigned()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnsafeNumber::class)
|
||||
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.errors.*
|
||||
import kotlin.test.*
|
||||
|
||||
internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
protected abstract val baseTempDir: MiraiFile // MiraiFile.create("/Users/Shared/mirai_test")
|
||||
protected abstract val tempPath: String
|
||||
private val tempDir by lazy {
|
||||
MiraiFile.create(tempPath).apply {
|
||||
assertTrue("Failed to make temp directory: ${this.absolutePath}") { mkdirs() }
|
||||
}
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun afterTest() {
|
||||
println("Cleaning up...")
|
||||
baseTempDir.deleteRecursively()
|
||||
}
|
||||
|
||||
@BeforeTest
|
||||
fun init() {
|
||||
println("Test start")
|
||||
assertTrue { tempDir.exists() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canonical paths for canonical input`() {
|
||||
assertEquals(tempPath, tempDir.path)
|
||||
assertEquals(tempPath, tempDir.absolutePath)
|
||||
}
|
||||
|
||||
@Test
|
||||
protected open fun parent() {
|
||||
assertEquals(tempDir, tempDir.resolve("s").parent)
|
||||
assertEquals(tempDir.parent, tempDir.resolve(".."))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canonical paths for non-canonical input`() {
|
||||
// extra /
|
||||
MiraiFile.create("$tempPath/").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
// extra //
|
||||
MiraiFile.create("$tempPath//").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
// extra /.
|
||||
MiraiFile.create("$tempPath/.").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${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)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
abstract fun `resolve absolute`()
|
||||
|
||||
@Test
|
||||
fun `exits createNewFile mkdir length`() {
|
||||
assertTrue { tempDir.exists() }
|
||||
|
||||
assertFalse { tempDir.resolve("not_existing_file.txt").exists() }
|
||||
assertEquals(0L, tempDir.resolve("not_existing_file.txt").length)
|
||||
assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() }
|
||||
assertEquals(0L, tempDir.resolve("not_existing_file.txt").length)
|
||||
assertTrue { tempDir.resolve("not_existing_file.txt").exists() }
|
||||
|
||||
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)
|
||||
assertTrue { tempDir.resolve("not_existing_dir").exists() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isFile isDirectory`() {
|
||||
assertTrue { tempDir.exists() }
|
||||
|
||||
assertFalse { tempDir.resolve("not_existing_file.txt").exists() }
|
||||
assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile)
|
||||
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
|
||||
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)
|
||||
assertTrue { tempDir.resolve("not_existing_file.txt").exists() }
|
||||
|
||||
assertFalse { tempDir.resolve("not_existing_dir").exists() }
|
||||
assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
|
||||
assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory)
|
||||
assertTrue { tempDir.resolve("not_existing_dir").mkdir() }
|
||||
assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
|
||||
assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory)
|
||||
assertTrue { tempDir.resolve("not_existing_dir").exists() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeText() {
|
||||
// new file
|
||||
tempDir.resolve("writeText1.txt").let { file ->
|
||||
val text = "some text"
|
||||
file.writeText(text)
|
||||
assertEquals(text.length, file.length.toInt())
|
||||
}
|
||||
|
||||
// override
|
||||
tempDir.resolve("writeText1.txt").let { file ->
|
||||
val text = "some other text"
|
||||
file.writeText(text)
|
||||
assertEquals(text.length, file.length.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readText() {
|
||||
tempDir.resolve("readText1.txt").let { file ->
|
||||
assertTrue { !file.exists() }
|
||||
assertFailsWith<IOException> { file.readText() }
|
||||
|
||||
val text = "some text"
|
||||
file.writeText(text)
|
||||
assertEquals(text, file.readText())
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CryptoTest {
|
||||
class ByteArrayOpTest {
|
||||
|
||||
@Test
|
||||
fun testAvailableProcessors() {
|
||||
@ -54,7 +54,7 @@ class CryptoTest {
|
||||
println(str)
|
||||
val hash = str.sha1()
|
||||
assertContentEquals(
|
||||
"54 98 CD 62 6C DE E3 9B 96 D4 34 5E 13 51 48 BB".hexToBytes(),
|
||||
"54 98 CD 62 6C DE E3 9B 96 D4 34 5E 13 51 48 BB FC 32 1C 48".hexToBytes(),
|
||||
hash,
|
||||
message = hash.toUHexString()
|
||||
)
|
||||
@ -90,7 +90,7 @@ class CryptoTest {
|
||||
println(str)
|
||||
val hash = str.toByteArray().gzip()
|
||||
assertContentEquals(
|
||||
"1F 8B 08 00 00 00 00 00 00 FF 2B 74 CF F3 32 0C 2A 72 73 B6 04 00 A8 35 6D D9 0A 00 00 00".hexToBytes(),
|
||||
"1F 8B 08 00 00 00 00 00 00 13 2B 74 CF F3 32 0C 2A 72 73 B6 04 00 A8 35 6D D9 0A 00 00 00".hexToBytes(),
|
||||
hash,
|
||||
message = hash.toUHexString()
|
||||
)
|
@ -10,56 +10,65 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.ktor.utils.io.errors.*
|
||||
import kotlinx.cinterop.UnsafeNumber
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
import platform.posix.*
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
private fun readlink(path: String): String = memScoped {
|
||||
val len = realpath(path, null)
|
||||
if (len != null) {
|
||||
try {
|
||||
return len.toKString()
|
||||
} finally {
|
||||
free(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 = "realpath()")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal actual class MiraiFileImpl actual constructor(
|
||||
absolutePath: String,
|
||||
override val path: String,
|
||||
) : MiraiFile {
|
||||
companion object {
|
||||
private const val SEPARATOR = "/"
|
||||
private const val SEPARATOR = '/'
|
||||
}
|
||||
|
||||
override val absolutePath: String by lazy { kotlin.run { readlink(path) } }
|
||||
|
||||
// TODO: 2022/5/28 normalize paths
|
||||
override val absolutePath: String = kotlin.run {
|
||||
absolutePath
|
||||
}
|
||||
|
||||
// TODO: 2022/5/28 normalize paths
|
||||
override val parent: MiraiFile? by lazy {
|
||||
val p = absolutePath.substringBeforeLast('/')
|
||||
val p = absolutePath.substringBeforeLast(SEPARATOR, "")
|
||||
if (p.isEmpty()) {
|
||||
return@lazy null
|
||||
}
|
||||
if (p.lastOrNull() == ':') {
|
||||
if (p.lastIndexOf('/') == p.lastIndex) {
|
||||
// C:/
|
||||
return@lazy null
|
||||
} else {
|
||||
return@lazy MiraiFileImpl("$p/") // C:/
|
||||
}
|
||||
}
|
||||
MiraiFileImpl(p)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = absolutePath.substringAfterLast('/').ifEmpty { absolutePath }
|
||||
get() = absolutePath.substringAfterLast('/', "").ifEmpty { absolutePath }
|
||||
|
||||
init {
|
||||
checkName(absolutePath.substringAfterLast('/')) // do not check drive letter
|
||||
absolutePath.split('/').forEach { checkName(it) }
|
||||
}
|
||||
|
||||
private fun checkName(name: String) {
|
||||
name.substringAfterLast('/').forEach { c ->
|
||||
name.forEach { c ->
|
||||
if (c in """\/:?*"><|""") {
|
||||
throw IllegalArgumentException("'${name}' contains illegal character '$c'.")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 2022/5/28 check name
|
||||
}
|
||||
|
||||
override val length: Long
|
||||
@ -75,19 +84,17 @@ internal actual class MiraiFileImpl actual constructor(
|
||||
|
||||
override fun exists(): Boolean = useStat { true } ?: false
|
||||
|
||||
// TODO: 2022/5/28 normalize paths
|
||||
override fun resolve(path: String): MiraiFile {
|
||||
when (path) {
|
||||
"." -> return this
|
||||
".." -> return parent ?: this // root
|
||||
}
|
||||
|
||||
if (path == "/") {
|
||||
if (path.startsWith(SEPARATOR)) {
|
||||
return MiraiFileImpl(path)
|
||||
}
|
||||
|
||||
val new = MiraiFileImpl(path)
|
||||
return MiraiFileImpl("$absolutePath${SEPARATOR}${new.parent}/${new.name}")
|
||||
return MiraiFileImpl("$absolutePath/$path")
|
||||
}
|
||||
|
||||
override fun resolve(file: MiraiFile): MiraiFile {
|
||||
@ -98,7 +105,6 @@ internal actual class MiraiFileImpl actual constructor(
|
||||
@OptIn(UnsafeNumber::class)
|
||||
override fun createNewFile(): Boolean {
|
||||
memScoped {
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
|
||||
val fp = fopen(absolutePath, "w")
|
||||
fwrite(fp, 0, 0, fp)
|
||||
fclose(fp)
|
||||
@ -115,29 +121,59 @@ internal actual class MiraiFileImpl actual constructor(
|
||||
}
|
||||
|
||||
override fun mkdir(): Boolean {
|
||||
memScoped {
|
||||
@Suppress("UnnecessaryOptInAnnotation") // bug
|
||||
@OptIn(UnsafeNumber::class)
|
||||
return mkdir(absolutePath, 0).convert<Int>() == 0
|
||||
}
|
||||
@Suppress("UnnecessaryOptInAnnotation") // bug
|
||||
@OptIn(UnsafeNumber::class)
|
||||
return mkdir(absolutePath, "755".toUShort(8).convert()).convert<Int>() == 0
|
||||
}
|
||||
|
||||
@OptIn(UnsafeNumber::class)
|
||||
override fun mkdirs(): Boolean {
|
||||
if (this.parent?.mkdirs() == false) {
|
||||
return false
|
||||
val flags = useStat { it.st_mode.convert<UInt>() }
|
||||
return when {
|
||||
flags == null -> {
|
||||
this.parent?.mkdirs()
|
||||
mkdir()
|
||||
}
|
||||
flags flag S_IFDIR -> {
|
||||
false // already exists
|
||||
}
|
||||
else -> {
|
||||
mkdir()
|
||||
}
|
||||
}
|
||||
return mkdir()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
override fun input(): Input {
|
||||
val handle = fopen(absolutePath, "r")
|
||||
if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'")
|
||||
return PosixInputForFile(handle!!)
|
||||
?: throw IOException(
|
||||
"Failed to open file '$absolutePath'",
|
||||
PosixException.forErrno(posixFunctionName = "fopen()")
|
||||
)
|
||||
return PosixInputForFile(handle)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
override fun output(): Output {
|
||||
val handle = fopen(absolutePath, "w")
|
||||
if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'")
|
||||
return PosixFileInstanceOutput(handle!!)
|
||||
?: throw IOException(
|
||||
"Failed to open file '$absolutePath'",
|
||||
PosixException.forErrno(posixFunctionName = "fopen()")
|
||||
)
|
||||
return PosixFileInstanceOutput(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)"
|
||||
}
|
||||
}
|
18
mirai-core-utils/src/unixMain/kotlin/StandardUtils.kt
Normal file
18
mirai-core-utils/src/unixMain/kotlin/StandardUtils.kt
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 kotlinx.cinterop.UnsafeNumber
|
||||
import kotlinx.cinterop.convert
|
||||
import platform.posix._SC_NPROCESSORS_ONLN
|
||||
import platform.posix.sysconf
|
||||
|
||||
@OptIn(UnsafeNumber::class)
|
||||
public actual fun availableProcessors(): Int = sysconf(_SC_NPROCESSORS_ONLN).convert()
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class UnixMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
|
||||
private val rand = Random.nextInt().absoluteValue
|
||||
override val baseTempDir: MiraiFile = MiraiFile.create("/Users/Shared/mirai_test")
|
||||
override val tempPath = "/Users/Shared/mirai_test/temp$rand"
|
||||
private val tempDir = MiraiFile.create(tempPath).apply {
|
||||
assertTrue("Failed to make temp directory: ${this.absolutePath}") { mkdirs() }
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun parent() {
|
||||
assertEquals("/Users/Shared/mirai_test", tempDir.parent!!.absolutePath)
|
||||
assertEquals(null, MiraiFile.create("/").parent)
|
||||
assertEquals("/", MiraiFile.create("/dev").parent?.path)
|
||||
assertEquals("/", MiraiFile.create("/dev").parent?.absolutePath)
|
||||
super.parent()
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun `resolve absolute`() {
|
||||
MiraiFile.create("$tempPath/").resolve("/Users").let {
|
||||
assertEquals("/Users", it.path)
|
||||
assertEquals("/Users", it.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
@ -52,7 +52,7 @@ internal class TimeBasedHeartbeatSchedulerImpl(
|
||||
val timeout = configuration.heartbeatTimeoutMillis
|
||||
|
||||
val list = mutableListOf<Job>()
|
||||
when (context[SsoProcessorContext].configuration.heartbeatStrategy) {
|
||||
when (val hb = context[SsoProcessorContext].configuration.heartbeatStrategy) {
|
||||
STAT_HB -> {
|
||||
list += launchHeartbeatJobAsync(
|
||||
scope = scope,
|
||||
@ -75,6 +75,7 @@ internal class TimeBasedHeartbeatSchedulerImpl(
|
||||
}
|
||||
NONE -> {
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected HeartbeatStrategy: $hb")
|
||||
}
|
||||
|
||||
list += launchHeartbeatJobAsync(
|
||||
|
@ -24,6 +24,7 @@ import net.mamoe.mirai.network.LoginFailedException
|
||||
import net.mamoe.mirai.network.RetryLaterException
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.jvm.JvmField
|
||||
import kotlin.native.concurrent.ThreadLocal
|
||||
|
||||
/**
|
||||
* A lazy stateful implementation of [NetworkHandlerSelector].
|
||||
@ -206,6 +207,8 @@ internal abstract class AbstractKeepAliveNetworkHandlerSelector<H : NetworkHandl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ThreadLocal
|
||||
companion object {
|
||||
@JvmField
|
||||
var DEFAULT_MAX_ATTEMPTS =
|
||||
|
Loading…
Reference in New Issue
Block a user