1
0
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:
Him188 2022-05-30 14:20:07 +01:00
parent 11a2354de4
commit 8d65a22f01
No known key found for this signature in database
GPG Key ID: BA439CDDCF652375
22 changed files with 855 additions and 111 deletions
mirai-core-api/src
commonMain/kotlin
nativeMain/kotlin/event
mirai-core-utils
mirai-core/src/commonMain/kotlin/network

View File

@ -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

View File

@ -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). 通常推荐这个模式.
*

View File

@ -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

View File

@ -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 {
}
}
}

View File

@ -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) }
}

View File

@ -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()

View File

@ -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

View File

@ -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 {

View 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()

View 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)
}
}
}

View File

@ -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)
// }
// }
//}

View 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()
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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())
}
}
}

View File

@ -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()
)

View File

@ -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)"
}
}

View 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()

View File

@ -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)
}
}
}

View File

@ -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(

View File

@ -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 =