diff --git a/mirai-core-utils/src/commonMain/kotlin/File.kt b/mirai-core-utils/src/commonMain/kotlin/File.kt index 74775a115..859355ab0 100644 --- a/mirai-core-utils/src/commonMain/kotlin/File.kt +++ b/mirai-core-utils/src/commonMain/kotlin/File.kt @@ -76,7 +76,7 @@ public fun MiraiFile.writeText(text: String) { } public fun MiraiFile.readText(): String { - return input().use { it.readText() } + return input().use { it.readAllText() } } public fun MiraiFile.readBytes(): ByteArray { diff --git a/mirai-core-utils/src/commonMain/kotlin/IO.kt b/mirai-core-utils/src/commonMain/kotlin/IO.kt index 3a9e38e20..48ad89b5f 100644 --- a/mirai-core-utils/src/commonMain/kotlin/IO.kt +++ b/mirai-core-utils/src/commonMain/kotlin/IO.kt @@ -167,6 +167,8 @@ public fun Input._readTLVMap( return map } +public fun Input.readAllText(): String = Charsets.UTF_8.newDecoder().decode(this) + public inline fun Input.readString(length: Int, charset: Charset = Charsets.UTF_8): String = String(this.readBytes(length), charset = charset) // stdlib diff --git a/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt b/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt index 1c09c4a82..e12bcaedf 100644 --- a/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt +++ b/mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt @@ -9,13 +9,46 @@ package net.mamoe.mirai.utils +import io.ktor.utils.io.bits.* import io.ktor.utils.io.core.* +import io.ktor.utils.io.errors.* import kotlinx.cinterop.* -import platform.posix.PATH_MAX -import platform.posix.fopen -import platform.posix.getcwd +import platform.posix.* import platform.windows.* +private fun getFullPathName(path: String): String = memScoped { + try { + println("getFullPathName") + ShortArray(MAX_PATH).usePinned { pin -> + val len = GetFullPathNameW(path, MAX_PATH, pin.addressOf(0).reinterpret(), null).toInt() + if (len != 0) { + return pin.get().toKStringFromUtf16(len) + } else { + when (val errno = errno) { + ENOTDIR -> return@memScoped path + EACCES -> return@memScoped path // permission denied + ENOENT -> return@memScoped path // no such file + else -> throw IllegalArgumentException( + "Invalid path($errno): $path", + cause = PosixException.forErrno(posixFunctionName = "GetFullPathNameW()") + ) + } + } + } + } finally { + println("getFullPathName finished") + } +} + +private fun ShortArray.toKStringFromUtf16(len: Int): String { + val chars = CharArray(len) + var index = 0 + while (index < len) { + chars[index] = this[index].toInt().toChar() + ++index + } + return chars.concatToString() +} internal actual class MiraiFileImpl actual constructor( // canonical @@ -40,14 +73,16 @@ internal actual class MiraiFileImpl actual constructor( } } - override val absolutePath: String = kotlin.run { - val result = ROOT_REGEX.matchEntire(path) ?: return@run path.dropLastWhile { it.isSeparator() } - return@run result.groups.first()!!.value + override val absolutePath: String by lazy { + val result = ROOT_REGEX.matchEntire(this.path) + ?: return@lazy getFullPathName(this.path).removeSuffix(SEPARATOR.toString()) + return@lazy result.groups.first()!!.value } private fun Char.isSeparator() = this == '/' || this == '\\' override val parent: MiraiFile? by lazy { + if (ROOT_REGEX.matchEntire(this.path) != null) return@lazy null val absolute = absolutePath val p = absolute.substringBeforeLast(SEPARATOR, "") if (p.isEmpty()) { @@ -66,41 +101,63 @@ internal actual class MiraiFileImpl actual constructor( override val name: String get() = if (absolutePath.matches(ROOT_REGEX)) absolutePath - else absolutePath.substringAfterLast('/') + else absolutePath.substringAfterLast(SEPARATOR) init { - checkName(absolutePath.substringAfterLast('/')) // do not check drive letter + checkName(absolutePath.substringAfterLast(SEPARATOR)) // do not check drive letter } private fun checkName(name: String) { - name.substringAfterLast('/').forEach { c -> + name.substringAfterLast(SEPARATOR).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.") - } - } +// 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 +// memScoped { +// val handle = CreateFileW( +// absolutePath, +// GENERIC_READ, +// FILE_SHARE_READ, +// null, +// OPEN_EXISTING, +// FILE_ATTRIBUTE_NORMAL, +// null +// ) ?: return@memScoped 0 +// val length = alloc<DWORDVar>() +// if (GetFileSize(handle, length.ptr) == INVALID_FILE_SIZE) { +// if (GetLastError() == NO_ERROR.toUInt()) { +// return INVALID_FILE_SIZE.convert() +// } +// throw PosixException.forErrno(posixFunctionName = "GetFileSize()").wrapIO() +// } +// if (CloseHandle(handle) == FALSE) { +// throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() +// } +// length.value.convert() +// } override val isFile: Boolean - get() = getFileAttributes() flag FILE_ATTRIBUTE_NORMAL + get() = useStat { it.st_mode.convert<UInt>() flag S_IFREG } ?: false override val isDirectory: Boolean - get() = getFileAttributes() flag FILE_ATTRIBUTE_DIRECTORY + get() = useStat { it.st_mode.convert<UInt>() flag S_IFDIR } ?: false override fun exists(): Boolean = getFileAttributes() != INVALID_FILE_ATTRIBUTES - private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesA(absolutePath) } + private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesW(absolutePath) } override fun resolve(path: String): MiraiFile { when (path) { @@ -121,54 +178,177 @@ internal actual class MiraiFileImpl actual constructor( } 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 + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea + val handle = CreateFileW( + absolutePath, + GENERIC_READ, + FILE_SHARE_DELETE, + null, + CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, + null + ) + if (handle == null || handle == INVALID_HANDLE_VALUE) { + return false } + if (CloseHandle(handle) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() + } + return true } override fun delete(): Boolean { return if (isFile) { - DeleteFileA(absolutePath) == 0 + DeleteFileW(absolutePath) != 0 } else { - RemoveDirectoryA(absolutePath) == 0 + RemoveDirectoryW(absolutePath) != 0 } } override fun mkdir(): Boolean { memScoped { val v = alloc<_SECURITY_ATTRIBUTES>() - return CreateDirectoryA(absolutePath, v.ptr) == 0 + return CreateDirectoryW(absolutePath, v.ptr) != 0 } } override fun mkdirs(): Boolean { - if (this.parent?.mkdirs() == false) { - return false - } + this.parent?.mkdirs() return mkdir() } override fun input(): Input { - val handle = fopen(absolutePath, "r") - if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'") - return PosixInputForFile(handle!!) +// println(absolutePath) +// val handle2 = fopen(absolutePath, "rb") ?:throw IOException( +// "Failed to open file '$absolutePath'", +// PosixException.forErrno(posixFunctionName = "fopen()") +// ) +// return PosixInputForFile(handle2) + // Will get I/O operation failed due to posix error code 2 + + val handle = CreateFileW( + absolutePath, + GENERIC_READ, + FILE_SHARE_DELETE, + null, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + null + ) + if (handle == null || handle == INVALID_HANDLE_VALUE) throw IOException( + "Failed to open file '$absolutePath'", + PosixException.forErrno(posixFunctionName = "CreateFileW()") + ) + return WindowsFileInput(handle) } override fun output(): Output { - val handle = fopen(absolutePath, "w") - if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'") - return PosixFileInstanceOutput(handle!!) +// val handle2 = fopen(absolutePath, "wb") +// ?: throw IOException( +// "Failed to open file '$absolutePath'", +// PosixException.forErrno(posixFunctionName = "fopen()") +// ) +// return PosixFileInstanceOutput(handle) +// + println(absolutePath) + val handle = CreateFileW( + absolutePath, + GENERIC_WRITE, + FILE_SHARE_DELETE, + null, + (if (exists()) TRUNCATE_EXISTING else CREATE_NEW).toUInt(), + FILE_ATTRIBUTE_NORMAL, + null + ) + if (handle == null || handle == INVALID_HANDLE_VALUE) throw IOException( + "Failed to open file '$absolutePath'", + PosixException.forErrno(posixFunctionName = "CreateFileW()") + ) + return WindowsFileOutput(handle) } -} \ No newline at end of file + + override fun hashCode(): Int { + return this.path.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other == null) return false + if (!isSameType(this, other)) return false + return this.path == other.path + } + + override fun toString(): String { + return "MiraiFileImpl($path)" + } +} + + +internal class WindowsFileInput(private val file: HANDLE) : Input() { + private var closed = false + + override fun fill(destination: Memory, offset: Int, length: Int): Int { + if (file == INVALID_HANDLE_VALUE) return 0 + + println("fill: ${destination.pointer}, $offset, $length") + memScoped { + val n = alloc<DWORDVar>() + if (ReadFile(file, destination.pointer + offset, length.convert(), n.ptr, null) == FALSE) { + println("ERR! LastErr= ${GetLastError()}") + throw PosixException.forErrno(posixFunctionName = "ReadFile()").wrapIO() + } + + println("LastErr= ${GetLastError()}") + println("${n.value}, ${n.value.convert<UInt>().toInt()}") + return n.value.convert<UInt>().toInt() + } + } + + override fun closeSource() { + println("closing") + if (closed) return + closed = true + + if (file != INVALID_HANDLE_VALUE) { + if (CloseHandle(file) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() + } + } + } +} + +@Suppress("DEPRECATION") +internal class WindowsFileOutput(private val file: HANDLE) : Output() { + private var closed = false + + override fun flush(source: Memory, offset: Int, length: Int) { + val end = offset + length + var currentOffset = offset + + memScoped { + val written = alloc<UIntVar>() + while (currentOffset < end) { + val result = WriteFile( + file, + source.pointer + currentOffset.convert(), + (end - currentOffset).convert(), + written.ptr, + null + ).convert<Int>() + if (result == FALSE) { + throw PosixException.forErrno(posixFunctionName = "WriteFile()").wrapIO() + } + currentOffset += written.value.toInt() + } + + } + } + + override fun closeDestination() { + if (closed) return + closed = true + + if (CloseHandle(file) == FALSE) { + throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO() + } + } +} diff --git a/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt b/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt index f8bfbee5d..9562dee6a 100644 --- a/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt +++ b/mirai-core-utils/src/mingwX64Test/kotlin/MiraiFileImplTest.kt @@ -16,20 +16,30 @@ import kotlin.test.assertEquals internal class WindowsMiraiFileImplTest : AbstractNativeMiraiFileImplTest() { private val rand = Random.nextInt().absoluteValue - override val baseTempDir: MiraiFile = MiraiFile.create("mirai_unit_tests") - override val tempPath = "mirai_unit_tests/temp$rand" + override val baseTempDir: MiraiFile = MiraiFile.create("C:\\mirai_unit_tests") + override val tempPath = "C:\\mirai_unit_tests\\temp$rand" @Test override fun parent() { - assertEquals("C:/Users/Shared/mirai_test", tempDir.parent!!.absolutePath) + assertEquals("C:\\mirai_unit_tests", tempDir.parent!!.absolutePath) super.parent() } + override fun `canonical paths for non-canonical input`() { + super.`canonical paths for non-canonical input`() + + // extra /sss/.. + MiraiFile.create("$tempPath/sss/..").resolve("test").let { + assertPathEquals("${tempPath}/test", it.path) // Windows resolves always + assertPathEquals("${tempPath}/test", it.absolutePath) + } + } + @Test override fun `resolve absolute`() { - MiraiFile.create("$tempPath/").resolve("C:/Users").let { - assertEquals("C:/Users", it.path) - assertEquals("C:/Users", it.absolutePath) + MiraiFile.create("$tempPath/").resolve("C:\\mirai_unit_tests").let { + assertEquals("C:\\mirai_unit_tests", it.path) + assertEquals("C:\\mirai_unit_tests", it.absolutePath) } } } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt b/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt index 88515a7e2..2c94a6236 100644 --- a/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt +++ b/mirai-core-utils/src/nativeTest/kotlin/AbstractNativeMiraiFileImplTest.kt @@ -24,7 +24,7 @@ internal abstract class AbstractNativeMiraiFileImplTest { @AfterTest fun afterTest() { println("Cleaning up...") - baseTempDir.deleteRecursively() + println("deleteRecursively:" + baseTempDir.deleteRecursively()) } @BeforeTest @@ -35,8 +35,8 @@ internal abstract class AbstractNativeMiraiFileImplTest { @Test fun `canonical paths for canonical input`() { - assertEquals(tempPath, tempDir.path) - assertEquals(tempPath, tempDir.absolutePath) + assertPathEquals(tempPath, tempDir.path) + assertPathEquals(tempPath, tempDir.absolutePath) } @Test @@ -46,31 +46,26 @@ internal abstract class AbstractNativeMiraiFileImplTest { } @Test - fun `canonical paths for non-canonical input`() { + open fun `canonical paths for non-canonical input`() { // extra / MiraiFile.create("$tempPath/").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } // extra // MiraiFile.create("$tempPath//").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } // extra /. MiraiFile.create("$tempPath/.").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } // extra /./. MiraiFile.create("$tempPath/./.").resolve("test").let { - assertEquals("${tempPath}/test", it.path) - assertEquals("${tempPath}/test", it.absolutePath) - } - // extra /sss/.. - MiraiFile.create("$tempPath/sss/..").resolve("test").let { - assertEquals("${tempPath}/sss/../test", it.path) // because file is not found - assertEquals("${tempPath}/sss/../test", it.absolutePath) + assertPathEquals("${tempPath}/test", it.path) + assertPathEquals("${tempPath}/test", it.absolutePath) } } @@ -90,7 +85,7 @@ internal abstract class AbstractNativeMiraiFileImplTest { assertFalse { tempDir.resolve("not_existing_dir").exists() } assertEquals(0L, tempDir.resolve("not_existing_dir").length) assertTrue { tempDir.resolve("not_existing_dir").mkdir() } - assertNotEquals(0L, tempDir.resolve("not_existing_dir").length) +// assertNotEquals(0L, tempDir.resolve("not_existing_dir").length) // length is platform-dependent, on Windows it is 0 but on unix it is not assertTrue { tempDir.resolve("not_existing_dir").exists() } } @@ -98,20 +93,27 @@ internal abstract class AbstractNativeMiraiFileImplTest { fun `isFile isDirectory`() { assertTrue { tempDir.exists() } + println("1") assertFalse { tempDir.resolve("not_existing_file.txt").exists() } assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile) + println("1") assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() } assertEquals(true, tempDir.resolve("not_existing_file.txt").isFile) assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_file.txt").exists() } + println("1") assertFalse { tempDir.resolve("not_existing_dir").exists() } assertEquals(false, tempDir.resolve("not_existing_dir").isFile) assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_dir").mkdir() } assertEquals(false, tempDir.resolve("not_existing_dir").isFile) assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory) + println("1") assertTrue { tempDir.resolve("not_existing_dir").exists() } } @@ -134,7 +136,7 @@ internal abstract class AbstractNativeMiraiFileImplTest { @Test fun readText() { - tempDir.resolve("readText1.txt").let { file -> + tempDir.resolve("readText2.txt").let { file -> assertTrue { !file.exists() } assertFailsWith<IOException> { file.readText() } @@ -143,4 +145,39 @@ internal abstract class AbstractNativeMiraiFileImplTest { assertEquals(text, file.readText()) } } + + private val bigText = "some text".repeat(10000) + + @Test + fun writeBigText() { + // new file + tempDir.resolve("writeText3.txt").let { file -> + file.writeText(bigText) + assertEquals(bigText.length, file.length.toInt()) + } + + // override + tempDir.resolve("writeText4.txt").let { file -> + file.writeText(bigText) + assertEquals(bigText.length, file.length.toInt()) + } + } + + @Test + fun readBigText() { + tempDir.resolve("readText4.txt").let { file -> + assertTrue { !file.exists() } + assertFailsWith<IOException> { file.readText() } + + file.writeText(bigText) + println("reading text") + val read = file.readText() + assertEquals(bigText.length, read.length) + assertEquals(bigText, read) + } + } + + protected fun assertPathEquals(expected: String, actual: String, message: String? = null) { + asserter.assertEquals(message, expected.replace("\\", "/"), actual.replace("\\", "/")) + } } \ No newline at end of file diff --git a/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt b/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt index 91d7751c8..05d17c5e0 100644 --- a/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt +++ b/mirai-core-utils/src/unixTest/kotlin/UnixMiraiFileImplTest.kt @@ -28,6 +28,16 @@ internal class UnixMiraiFileImplTest : AbstractNativeMiraiFileImplTest() { super.parent() } + override fun `canonical paths for non-canonical input`() { + super.`canonical paths for non-canonical input`() + + // extra /sss/.. + MiraiFile.create("$tempPath/sss/..").resolve("test").let { + assertPathEquals("${tempPath}/sss/../test", it.path) // because file is not found + assertPathEquals("${tempPath}/sss/../test", it.absolutePath) + } + } + @Test override fun `resolve absolute`() { MiraiFile.create("$tempPath/").resolve("/Users").let {