1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-25 13:03:35 +08:00

MiraiFile

This commit is contained in:
Him188 2022-05-28 23:03:18 +01:00
parent 7e9e248f2c
commit c1335f8941
No known key found for this signature in database
GPG Key ID: BA439CDDCF652375
5 changed files with 476 additions and 4 deletions
mirai-core-api/src/nativeMain/kotlin/utils
mirai-core-utils/src

View File

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

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

View File

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

View File

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

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