mirror of
https://github.com/mamoe/mirai.git
synced 2025-04-25 13:03:35 +08:00
MiraiFile
This commit is contained in:
parent
7e9e248f2c
commit
c1335f8941
mirai-core-api/src/nativeMain/kotlin/utils
mirai-core-utils/src
mingwMain/kotlin
nativeMain/kotlin
unixMain/kotlin
@ -327,7 +327,10 @@ public actual open class BotConfiguration { // open for Java
|
||||
*/
|
||||
@ConfigurationDsl
|
||||
public actual fun fileBasedDeviceInfo(filepath: String) {
|
||||
deviceInfo = TODO("native")
|
||||
deviceInfo = {
|
||||
val file = MiraiFile.create(workingDir).resolve(filepath)
|
||||
Json.decodeFromString(DeviceInfo.serializer(), file.readText())
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
160
mirai-core-utils/src/mingwMain/kotlin/MiraiFileImpl.kt
Normal file
160
mirai-core-utils/src/mingwMain/kotlin/MiraiFileImpl.kt
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.fopen
|
||||
import platform.windows.*
|
||||
|
||||
|
||||
internal actual class MiraiFileImpl actual constructor(
|
||||
// canonical
|
||||
absolutePath: String
|
||||
) : MiraiFile {
|
||||
companion object {
|
||||
private val ROOT_REGEX = Regex("""^([a-zA-z]+:[/\\])""")
|
||||
private const val SEPARATOR = "\\"
|
||||
}
|
||||
|
||||
override val absolutePath: String = kotlin.run {
|
||||
val result = ROOT_REGEX.matchEntire(absolutePath) ?: return@run absolutePath.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('/')
|
||||
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() = 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<WINBOOLVar>()
|
||||
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) }
|
||||
|
||||
// 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)) {
|
||||
return MiraiFileImpl(path)
|
||||
}
|
||||
|
||||
val new = MiraiFileImpl(path)
|
||||
return MiraiFileImpl("$absolutePath${SEPARATOR}${new.parent}/${new.name}")
|
||||
}
|
||||
|
||||
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!!)
|
||||
}
|
||||
}
|
@ -11,7 +11,18 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Multiplatform implementation of file operations.
|
||||
@ -34,8 +45,142 @@ public actual interface MiraiFile {
|
||||
public actual fun output(): Output
|
||||
|
||||
public actual companion object {
|
||||
public actual fun create(absolutePath: String): MiraiFile {
|
||||
TODO("Not yet implemented")
|
||||
public actual fun create(absolutePath: String): MiraiFile = MiraiFileImpl(absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal expect class MiraiFileImpl(
|
||||
absolutePath: String,
|
||||
) : MiraiFile
|
||||
|
||||
|
||||
/*
|
||||
Data from https://man7.org/linux/man-pages/man2/lstat.2.html
|
||||
|
||||
|
||||
st_dev This field describes the device on which this file
|
||||
resides. (The major(3) and minor(3) macros may be useful
|
||||
to decompose the device ID in this field.)
|
||||
|
||||
st_ino This field contains the file's inode number.
|
||||
|
||||
st_mode
|
||||
This field contains the file type and mode. See inode(7)
|
||||
for further information.
|
||||
|
||||
S_IFMT 0170000 bit mask for the file type bit field
|
||||
|
||||
S_IFSOCK 0140000 socket
|
||||
S_IFLNK 0120000 symbolic link
|
||||
S_IFREG 0100000 regular file
|
||||
S_IFBLK 0060000 block device
|
||||
S_IFDIR 0040000 directory
|
||||
S_IFCHR 0020000 character device
|
||||
S_IFIFO 0010000 FIFO
|
||||
|
||||
|
||||
st_nlink
|
||||
This field contains the number of hard links to the file.
|
||||
|
||||
st_uid This field contains the user ID of the owner of the file.
|
||||
|
||||
st_gid This field contains the ID of the group owner of the file.
|
||||
|
||||
st_rdev
|
||||
This field describes the device that this file (inode)
|
||||
represents.
|
||||
|
||||
st_size
|
||||
This field gives the size of the file (if it is a regular
|
||||
file or a symbolic link) in bytes. The size of a symbolic
|
||||
link is the length of the pathname it contains, without a
|
||||
terminating null byte.
|
||||
|
||||
st_blksize
|
||||
This field gives the "preferred" block size for efficient
|
||||
filesystem I/O.
|
||||
|
||||
st_blocks
|
||||
This field indicates the number of blocks allocated to the
|
||||
file, in 512-byte units. (This may be smaller than
|
||||
st_size/512 when the file has holes.)
|
||||
|
||||
st_atime
|
||||
This is the time of the last access of file data.
|
||||
|
||||
st_mtime
|
||||
This is the time of last modification of file data.
|
||||
|
||||
st_ctime
|
||||
This is the file's last status change timestamp (time of
|
||||
last change to the inode).
|
||||
|
||||
*/
|
||||
internal inline fun <R> MiraiFileImpl.useStat(block: (stat) -> R): R? {
|
||||
memScoped {
|
||||
val stat = alloc<stat>()
|
||||
val ret = stat(absolutePath, stat.ptr)
|
||||
if (ret != 0) return null
|
||||
return block(stat)
|
||||
}
|
||||
}
|
||||
|
||||
internal class FileNotFoundException(message: String, cause: Throwable? = null) :
|
||||
IOException(message, cause)
|
||||
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
internal class PosixFileInstanceOutput(val file: CPointer<FILE>) : AbstractOutput() {
|
||||
private var closed = false
|
||||
|
||||
override fun flush(source: Memory, offset: Int, length: Int) {
|
||||
val end = offset + length
|
||||
var currentOffset = offset
|
||||
|
||||
while (currentOffset < end) {
|
||||
val result = fwrite(source, currentOffset, end - currentOffset, file.cast())
|
||||
if (result == 0) {
|
||||
throw PosixException.forErrno(posixFunctionName = "fwrite()").wrapIO()
|
||||
}
|
||||
currentOffset += result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeDestination() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
|
||||
if (fclose(file) != 0) {
|
||||
throw PosixException.forErrno(posixFunctionName = "fclose").wrapIO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
internal class PosixInputForFile(val file: CPointer<FILE>) : AbstractInput() {
|
||||
private var closed = false
|
||||
|
||||
override fun fill(destination: Memory, offset: Int, length: Int): Int {
|
||||
val size = fread(destination, offset, length, file.cast())
|
||||
if (size == 0) {
|
||||
if (feof(file) != 0) return 0
|
||||
throw PosixException.forErrno(posixFunctionName = "read()").wrapIO()
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
override fun closeSource() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
|
||||
if (fclose(file) != 0) {
|
||||
throw PosixException.forErrno(posixFunctionName = "fclose()").wrapIO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
internal fun PosixException.wrapIO(): IOException =
|
||||
IOException("I/O operation failed due to posix error code $errno", this)
|
||||
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.*
|
||||
|
||||
public inline infix fun UShort.flag(flag: UShort): Boolean = this and flag != 0u.toUShort()
|
||||
public inline infix fun UInt.flag(flag: UInt): Boolean = this and flag != 0u
|
||||
public inline infix fun UInt.flag(flag: Int): Boolean = this and flag.toUInt() != 0u
|
||||
public inline infix fun Int.flag(flag: UInt): Boolean = this.toUInt() and flag != 0u
|
||||
public inline infix fun ULong.flag(flag: ULong): Boolean = this and flag != 0uL
|
||||
|
||||
public val NULL_PTR: COpaquePointerVar = nativeHeap.alloc()
|
||||
public inline fun <reified T : NativePointed> nullPtr(): T = NULL_PTR.reinterpret()
|
143
mirai-core-utils/src/unixMain/kotlin/MiraiFileCrossPlatform.kt
Normal file
143
mirai-core-utils/src/unixMain/kotlin/MiraiFileCrossPlatform.kt
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.UnsafeNumber
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.memScoped
|
||||
import platform.posix.*
|
||||
|
||||
internal actual class MiraiFileImpl actual constructor(
|
||||
absolutePath: String,
|
||||
) : MiraiFile {
|
||||
companion object {
|
||||
private const val SEPARATOR = "/"
|
||||
}
|
||||
|
||||
|
||||
// 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('/')
|
||||
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 }
|
||||
|
||||
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'.")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 2022/5/28 check name
|
||||
}
|
||||
|
||||
override val length: Long
|
||||
get() = useStat { it.st_size.convert() } ?: 0
|
||||
|
||||
@OptIn(UnsafeNumber::class)
|
||||
override val isFile: Boolean
|
||||
get() = useStat { it.st_mode.convert<UInt>() flag S_IFREG } ?: false
|
||||
|
||||
@OptIn(UnsafeNumber::class)
|
||||
override val isDirectory: Boolean
|
||||
get() = useStat { it.st_mode.convert<UInt>() flag S_IFDIR } ?: false
|
||||
|
||||
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 == "/") {
|
||||
return MiraiFileImpl(path)
|
||||
}
|
||||
|
||||
val new = MiraiFileImpl(path)
|
||||
return MiraiFileImpl("$absolutePath${SEPARATOR}${new.parent}/${new.name}")
|
||||
}
|
||||
|
||||
override fun resolve(file: MiraiFile): MiraiFile {
|
||||
val parent = file.parent ?: return resolve(file.name)
|
||||
return resolve(parent).resolve(file.name)
|
||||
}
|
||||
|
||||
@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)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(): Boolean {
|
||||
return if (isFile) {
|
||||
remove(absolutePath) == 0
|
||||
} else {
|
||||
rmdir(absolutePath) == 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun mkdir(): Boolean {
|
||||
memScoped {
|
||||
@Suppress("UnnecessaryOptInAnnotation") // bug
|
||||
@OptIn(UnsafeNumber::class)
|
||||
return mkdir(absolutePath, 0).convert<Int>() == 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!!)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user