From eba0b522754ea02d6c2de7413b6cfef9dcec7825 Mon Sep 17 00:00:00 2001 From: iseki Date: Mon, 29 Sep 2025 03:22:55 +0800 Subject: [PATCH 01/16] Fix Windows target path encoding problem --- .../kotlinx-io-multiplatform.gradle.kts | 6 -- core/apple/src/files/FileSystemApple.kt | 52 +++++++++- core/linux/src/files/FileSystemLinux.kt | 34 +++++++ core/mingw/src/files/Error.kt | 38 ++++++++ core/mingw/src/files/FileSystemMingw.kt | 96 +++++++++++++++++-- .../src/files/FileSystemNativeNonAndroid.kt | 42 -------- 6 files changed, 211 insertions(+), 57 deletions(-) create mode 100644 core/mingw/src/files/Error.kt delete mode 100644 core/nativeNonAndroid/src/files/FileSystemNativeNonAndroid.kt diff --git a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts index 05b978353..26a7e0cf5 100644 --- a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts +++ b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts @@ -97,12 +97,6 @@ kotlin { group("androidNative") } } - - group("nativeNonAndroid") { - group("apple") - group("mingw") - group("linux") - } } group("nodeFilesystemShared") { withJs() diff --git a/core/apple/src/files/FileSystemApple.kt b/core/apple/src/files/FileSystemApple.kt index 75882b967..e13664ca8 100644 --- a/core/apple/src/files/FileSystemApple.kt +++ b/core/apple/src/files/FileSystemApple.kt @@ -6,10 +6,29 @@ package kotlinx.io.files -import kotlinx.cinterop.* +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.cstr +import kotlinx.cinterop.get +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString import kotlinx.io.IOException -import platform.Foundation.* -import platform.posix.* +import platform.Foundation.NSFileManager +import platform.Foundation.NSFileSize +import platform.Foundation.NSFileType +import platform.Foundation.NSFileTypeDirectory +import platform.Foundation.NSFileTypeRegular +import platform.Foundation.NSTemporaryDirectory +import platform.posix.DIR +import platform.posix.basename +import platform.posix.closedir +import platform.posix.dirname +import platform.posix.errno +import platform.posix.free +import platform.posix.mkdir +import platform.posix.realpath +import platform.posix.rename +import platform.posix.strerror internal actual fun atomicMoveImpl(source: Path, destination: Path) { @@ -64,3 +83,30 @@ internal actual fun metadataOrNullImpl(path: Path): FileMetadata? { size = if (isFile) attributes[NSFileSize] as Long else -1 ) } + +@OptIn(ExperimentalForeignApi::class) +internal actual class OpaqueDirEntry(private val dir: CPointer) : AutoCloseable { + actual fun readdir(): String? { + val entry = platform.posix.readdir(dir) ?: return null + return entry[0].d_name.toKString() + } + + actual override fun close() { + if (closedir(dir) != 0) { + val err = errno + val strerr = strerror(err)?.toKString() ?: "unknown error" + throw IOException("closedir failed with errno $err ($strerr)") + } + } +} + +@OptIn(ExperimentalForeignApi::class) +internal actual fun opendir(path: String): OpaqueDirEntry { + val dirent = platform.posix.opendir(path) + if (dirent != null) return OpaqueDirEntry(dirent) + + val err = errno + val strerr = strerror(err)?.toKString() ?: "unknown error" + throw IOException("Can't open directory $path: $err ($strerr)") +} + diff --git a/core/linux/src/files/FileSystemLinux.kt b/core/linux/src/files/FileSystemLinux.kt index 5c46e71f4..00ca4330c 100644 --- a/core/linux/src/files/FileSystemLinux.kt +++ b/core/linux/src/files/FileSystemLinux.kt @@ -5,12 +5,19 @@ package kotlinx.io.files +import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cstr +import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.toKString +import kotlinx.io.IOException +import platform.posix.DIR import platform.posix.__xpg_basename +import platform.posix.closedir import platform.posix.dirname +import platform.posix.errno +import platform.posix.strerror @OptIn(ExperimentalForeignApi::class) internal actual fun dirnameImpl(path: String): String { @@ -30,3 +37,30 @@ internal actual fun basenameImpl(path: String): String { } internal actual fun isAbsoluteImpl(path: String): Boolean = path.startsWith('/') + + +@OptIn(ExperimentalForeignApi::class) +internal actual class OpaqueDirEntry(private val dir: CPointer) : AutoCloseable { + actual fun readdir(): String? { + val entry = platform.posix.readdir(dir) ?: return null + return entry[0].d_name.toKString() + } + + actual override fun close() { + if (closedir(dir) != 0) { + val err = errno + val strerr = strerror(err)?.toKString() ?: "unknown error" + throw IOException("closedir failed with errno $err ($strerr)") + } + } +} + +@OptIn(ExperimentalForeignApi::class) +internal actual fun opendir(path: String): OpaqueDirEntry { + val dirent = platform.posix.opendir(path) + if (dirent != null) return OpaqueDirEntry(dirent) + + val err = errno + val strerr = strerror(err)?.toKString() ?: "unknown error" + throw IOException("Can't open directory $path: $err ($strerr)") +} diff --git a/core/mingw/src/files/Error.kt b/core/mingw/src/files/Error.kt new file mode 100644 index 000000000..3556d2d5f --- /dev/null +++ b/core/mingw/src/files/Error.kt @@ -0,0 +1,38 @@ +package kotlinx.io.files + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.toKString +import kotlinx.cinterop.value +import platform.windows.FORMAT_MESSAGE_ALLOCATE_BUFFER +import platform.windows.FORMAT_MESSAGE_FROM_SYSTEM +import platform.windows.FORMAT_MESSAGE_IGNORE_INSERTS +import platform.windows.FormatMessageW +import platform.windows.LPWSTRVar +import platform.windows.LocalFree + +@OptIn(ExperimentalForeignApi::class) +internal fun formatWin32ErrorMessage(code: UInt): String { + memScoped { + val r = alloc() + val n = FormatMessageW( + dwFlags = (FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_IGNORE_INSERTS or FORMAT_MESSAGE_FROM_SYSTEM).toUInt(), + lpSource = null, + dwMessageId = code, + dwLanguageId = 0u, + lpBuffer = r.ptr.reinterpret(), + nSize = 0u, + Arguments = null, + ) + if (n == 0u) { + throw RuntimeException("Error formatting error: $code") + } + val s = r.value?.toKString().orEmpty() + LocalFree(r.value) + return s + } + +} diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index e7ad2cc8a..47c8c3676 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -7,10 +7,37 @@ package kotlinx.io.files -import kotlinx.cinterop.* +import kotlinx.cinterop.Arena +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert +import kotlinx.cinterop.cstr +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString import kotlinx.io.IOException -import platform.posix.* -import platform.windows.* +import platform.posix.basename +import platform.posix.dirname +import platform.posix.errno +import platform.posix.mkdir +import platform.posix.strerror +import platform.windows.CloseHandle +import platform.windows.ERROR_FILE_NOT_FOUND +import platform.windows.ERROR_NO_MORE_FILES +import platform.windows.FindClose +import platform.windows.FindFirstFileW +import platform.windows.FindNextFileW +import platform.windows.GetFullPathNameW +import platform.windows.GetLastError +import platform.windows.HANDLE +import platform.windows.INVALID_HANDLE_VALUE +import platform.windows.MOVEFILE_REPLACE_EXISTING +import platform.windows.MoveFileExA +import platform.windows.PathIsRelativeW +import platform.windows.TRUE +import platform.windows.WCHARVar +import platform.windows.WIN32_FIND_DATAW internal actual fun atomicMoveImpl(source: Path, destination: Path) { if (MoveFileExA(source.path, destination.path, MOVEFILE_REPLACE_EXISTING.convert()) == 0) { @@ -41,7 +68,7 @@ internal actual fun isAbsoluteImpl(path: String): Boolean { val next = path[2] return next == WindowsPathSeparator || next == SystemPathSeparator } - return PathIsRelativeA(path) == 0 + return PathIsRelativeW(path) == 0 } internal actual fun mkdirImpl(path: String) { @@ -54,9 +81,66 @@ private const val MAX_PATH_LENGTH = 32767 internal actual fun realpathImpl(path: String): String { memScoped { - val buffer = allocArray(MAX_PATH_LENGTH) - val len = GetFullPathNameA(path, MAX_PATH_LENGTH.convert(), buffer, null) + val buffer = allocArray(MAX_PATH_LENGTH) + val len = GetFullPathNameW(path, MAX_PATH_LENGTH.convert(), buffer, null) if (len == 0u) throw IllegalStateException() return buffer.toKString() } } + +internal actual class OpaqueDirEntry(directory: String) : AutoCloseable { + private val arena = Arena() + private val data = arena.alloc() + private var handle: HANDLE? = INVALID_HANDLE_VALUE + private var firstName: String? = null + + init { + try { + val directory0 = if (directory.endsWith('/') || directory.endsWith('\\')) "$directory*" else "$directory/*" + handle = FindFirstFileW(directory0, data.ptr) + if (handle != INVALID_HANDLE_VALUE) { + firstName = data.cFileName.toKString() + } else { + val le = GetLastError() + if (le != ERROR_FILE_NOT_FOUND.toUInt()) { + val strerr = formatWin32ErrorMessage(le) + throw IOException("Can't open directory $directory: $le ($strerr)") + } + } + } catch (th: Throwable) { + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle) + } + arena.clear() + throw th + } + } + + actual fun readdir(): String? { + if (firstName != null) { + return firstName.also { firstName = null } + } + if (handle == INVALID_HANDLE_VALUE) { + return null + } + if (FindNextFileW(handle, data.ptr) == TRUE) { + return data.cFileName.toKString() + } + val le = GetLastError() + if (le == ERROR_NO_MORE_FILES.toUInt()) { + return null + } + val strerr = formatWin32ErrorMessage(le) + throw IOException("Can't readdir: $le ($strerr)") + } + + actual override fun close() { + if (handle != INVALID_HANDLE_VALUE) { + FindClose(handle) + } + arena.clear() + } + +} + +internal actual fun opendir(path: String): OpaqueDirEntry = OpaqueDirEntry(path) diff --git a/core/nativeNonAndroid/src/files/FileSystemNativeNonAndroid.kt b/core/nativeNonAndroid/src/files/FileSystemNativeNonAndroid.kt deleted file mode 100644 index a75172231..000000000 --- a/core/nativeNonAndroid/src/files/FileSystemNativeNonAndroid.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package kotlinx.io.files - -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.get -import kotlinx.cinterop.toKString -import kotlinx.io.IOException -import platform.posix.DIR -import platform.posix.closedir -import platform.posix.errno -import platform.posix.strerror - -@OptIn(ExperimentalForeignApi::class) -internal actual class OpaqueDirEntry(private val dir: CPointer) : AutoCloseable { - actual fun readdir(): String? { - val entry = platform.posix.readdir(dir) ?: return null - return entry[0].d_name.toKString() - } - - actual override fun close() { - if (closedir(dir) != 0) { - val err = errno - val strerr = strerror(err)?.toKString() ?: "unknown error" - throw IOException("closedir failed with errno $err ($strerr)") - } - } -} - -@OptIn(ExperimentalForeignApi::class) -internal actual fun opendir(path: String): OpaqueDirEntry { - val dirent = platform.posix.opendir(path) - if (dirent != null) return OpaqueDirEntry(dirent) - - val err = errno - val strerr = strerror(err)?.toKString() ?: "unknown error" - throw IOException("Can't open directory $path: $err ($strerr)") -} From 3540e98d6ef80b15669065835b85c895ca045315 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 02:27:21 +0800 Subject: [PATCH 02/16] WIP --- core/mingw/src/files/Error.kt | 9 +- core/mingw/src/files/FileSystemMingw.kt | 101 ++++++++++++------ .../test/files/SmokeFileTestWindowsMinGW.kt | 69 +++++++++++- core/mingw/testdir/foo | 0 ...1\204\343\202\215\343\201\257\346\255\214" | 0 ...4\251\345\234\260\347\216\204\351\273\204" | 0 6 files changed, 139 insertions(+), 40 deletions(-) create mode 100644 core/mingw/testdir/foo create mode 100644 "core/mingw/testdir/\343\201\204\343\202\215\343\201\257\346\255\214" create mode 100644 "core/mingw/testdir/\345\244\251\345\234\260\347\216\204\351\273\204" diff --git a/core/mingw/src/files/Error.kt b/core/mingw/src/files/Error.kt index 3556d2d5f..19a3048f9 100644 --- a/core/mingw/src/files/Error.kt +++ b/core/mingw/src/files/Error.kt @@ -11,11 +11,12 @@ import platform.windows.FORMAT_MESSAGE_ALLOCATE_BUFFER import platform.windows.FORMAT_MESSAGE_FROM_SYSTEM import platform.windows.FORMAT_MESSAGE_IGNORE_INSERTS import platform.windows.FormatMessageW +import platform.windows.GetLastError import platform.windows.LPWSTRVar import platform.windows.LocalFree @OptIn(ExperimentalForeignApi::class) -internal fun formatWin32ErrorMessage(code: UInt): String { +internal fun formatWin32ErrorMessage(code: UInt = GetLastError()): String { memScoped { val r = alloc() val n = FormatMessageW( @@ -28,11 +29,11 @@ internal fun formatWin32ErrorMessage(code: UInt): String { Arguments = null, ) if (n == 0u) { - throw RuntimeException("Error formatting error: $code") + return "unknown error (${code.toHexString()})" } - val s = r.value?.toKString().orEmpty() + val s = r.value!!.toKString().trimEnd() LocalFree(r.value) - return s + return "$s (${code.toHexString()})" } } diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index 47c8c3676..f6b45c680 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -8,57 +8,86 @@ package kotlinx.io.files import kotlinx.cinterop.Arena +import kotlinx.cinterop.CFunction +import kotlinx.cinterop.CPointed +import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.alloc import kotlinx.cinterop.allocArray import kotlinx.cinterop.convert -import kotlinx.cinterop.cstr +import kotlinx.cinterop.invoke import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret import kotlinx.cinterop.toKString +import kotlinx.cinterop.wcstr import kotlinx.io.IOException -import platform.posix.basename -import platform.posix.dirname -import platform.posix.errno -import platform.posix.mkdir -import platform.posix.strerror +import platform.posix.size_t import platform.windows.CloseHandle +import platform.windows.CreateDirectoryW import platform.windows.ERROR_FILE_NOT_FOUND import platform.windows.ERROR_NO_MORE_FILES +import platform.windows.FALSE import platform.windows.FindClose import platform.windows.FindFirstFileW import platform.windows.FindNextFileW import platform.windows.GetFullPathNameW import platform.windows.GetLastError +import platform.windows.GetProcAddress import platform.windows.HANDLE +import platform.windows.HMODULE +import platform.windows.HRESULT import platform.windows.INVALID_HANDLE_VALUE +import platform.windows.LoadLibraryW +import platform.windows.MAX_PATH import platform.windows.MOVEFILE_REPLACE_EXISTING -import platform.windows.MoveFileExA +import platform.windows.MoveFileExW +import platform.windows.PWSTR +import platform.windows.PathFindFileNameW import platform.windows.PathIsRelativeW +import platform.windows.PathIsRootW import platform.windows.TRUE import platform.windows.WCHARVar import platform.windows.WIN32_FIND_DATAW +import kotlin.experimental.ExperimentalNativeApi + +private typealias PathCchRemoveFileSpecFunc = CPointer HRESULT>> + +@OptIn(ExperimentalNativeApi::class) +private val kernelBaseDll = LoadLibraryW("kernelbase.dll") ?: run { + terminateWithUnhandledException(RuntimeException("kernelbase_dll is not supported: ${formatWin32ErrorMessage()}")) +} + +@OptIn(ExperimentalNativeApi::class) +private fun getProcAddressOrFailed(module: HMODULE, name: String): CPointer { + val pointer = GetProcAddress(kernelBaseDll, "PathCchRemoveFileSpec") ?: terminateWithUnhandledException( + UnsupportedOperationException("Failed to get proc: $name: ${formatWin32ErrorMessage()}"), + ) + return pointer.reinterpret() +} + +// Available since Windows 8 / Windows Server 2012, long path and UNC path supported +private val PathCchRemoveFileSpec: PathCchRemoveFileSpecFunc = + getProcAddressOrFailed(kernelBaseDll, "PathCchRemoveFileSpec") internal actual fun atomicMoveImpl(source: Path, destination: Path) { - if (MoveFileExA(source.path, destination.path, MOVEFILE_REPLACE_EXISTING.convert()) == 0) { - // TODO: get formatted error message - throw IOException("Move failed with error code: ${GetLastError()}") + if (MoveFileExW(source.path, destination.path, MOVEFILE_REPLACE_EXISTING.convert()) == 0) { + throw IOException("Move failed with error code: ${formatWin32ErrorMessage()}") } } internal actual fun dirnameImpl(path: String): String { - if (!path.contains(UnixPathSeparator) && !path.contains(WindowsPathSeparator)) { - return "" - } memScoped { - return dirname(path.cstr.ptr)?.toKString() ?: "" + val p = path.wcstr.ptr + // we don't care the result, even it failed. + PathCchRemoveFileSpec.invoke(p, path.length.convert()) + return p.toKString() } } internal actual fun basenameImpl(path: String): String { - memScoped { - return basename(path.cstr.ptr)?.toKString() ?: "" - } + if (PathIsRootW(path) == TRUE) return "" + return PathFindFileNameW(path)?.toKString() ?: "" } internal actual fun isAbsoluteImpl(path: String): Boolean { @@ -72,23 +101,29 @@ internal actual fun isAbsoluteImpl(path: String): Boolean { } internal actual fun mkdirImpl(path: String) { - if (mkdir(path) != 0) { - throw IOException("mkdir failed: ${strerror(errno)?.toKString()}") + if (CreateDirectoryW(path, null) == FALSE) { + throw IOException("mkdir failed: $path: ${formatWin32ErrorMessage()}") } } -private const val MAX_PATH_LENGTH = 32767 - internal actual fun realpathImpl(path: String): String { memScoped { - val buffer = allocArray(MAX_PATH_LENGTH) - val len = GetFullPathNameW(path, MAX_PATH_LENGTH.convert(), buffer, null) - if (len == 0u) throw IllegalStateException() - return buffer.toKString() + // in practice, MAX_PATH is enough for most cases + var buf = allocArray(MAX_PATH) + var r = GetFullPathNameW(path, MAX_PATH.convert(), buf, null) + if (r >= MAX_PATH.toUInt()) { + // if not, we will retry with the required size + buf = allocArray(r.toInt()) + r = GetFullPathNameW(path, r, buf, null) + } + if (r == 0u) { + error("GetFullPathNameW failed for $path: ${formatWin32ErrorMessage()}") + } + return buf.toKString() } } -internal actual class OpaqueDirEntry(directory: String) : AutoCloseable { +internal actual class OpaqueDirEntry(private val directory: String) : AutoCloseable { private val arena = Arena() private val data = arena.alloc() private var handle: HANDLE? = INVALID_HANDLE_VALUE @@ -96,15 +131,16 @@ internal actual class OpaqueDirEntry(directory: String) : AutoCloseable { init { try { - val directory0 = if (directory.endsWith('/') || directory.endsWith('\\')) "$directory*" else "$directory/*" + // since the root + val directory0 = + if (directory.endsWith(UnixPathSeparator) || directory.endsWith(WindowsPathSeparator)) "$directory*" else "$directory/*" handle = FindFirstFileW(directory0, data.ptr) if (handle != INVALID_HANDLE_VALUE) { firstName = data.cFileName.toKString() } else { - val le = GetLastError() - if (le != ERROR_FILE_NOT_FOUND.toUInt()) { - val strerr = formatWin32ErrorMessage(le) - throw IOException("Can't open directory $directory: $le ($strerr)") + val e = GetLastError() + if (e != ERROR_FILE_NOT_FOUND.toUInt()) { + throw IOException("Can't open directory $directory: ${formatWin32ErrorMessage(e)}") } } } catch (th: Throwable) { @@ -130,8 +166,7 @@ internal actual class OpaqueDirEntry(directory: String) : AutoCloseable { if (le == ERROR_NO_MORE_FILES.toUInt()) { return null } - val strerr = formatWin32ErrorMessage(le) - throw IOException("Can't readdir: $le ($strerr)") + throw IOException("Can't readdir from $directory: ${formatWin32ErrorMessage(le)}") } actual override fun close() { diff --git a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt index 2dcf04021..c6c9debe5 100644 --- a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt +++ b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt @@ -5,13 +5,76 @@ package kotlinx.io.files +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.cstr +import kotlinx.cinterop.toKString +import platform.posix.dirname +import platform.windows.ERROR_TOO_MANY_OPEN_FILES import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFails + +class SmokeFileTestWindowsMinGW { + @OptIn(ExperimentalForeignApi::class) + @Test + fun mingwProblem() { + assertEquals("""C:\foo""", dirname("""C:\foo\bar""".cstr)!!.toKString()) + assertFails { + assertEquals( + """C:\あいうえお""", + dirname("""C:\あいうえお\かきくけこ""".cstr)!!.toKString(), + ) + }.let(::println) + assertFails { + assertEquals( + """C:\一二三四""", + dirname("""C:\一二三四\五六七八""".cstr)!!.toKString(), + ) + }.let(::println) + } + + @Test + fun parent() { + assertEquals(Path("""C:\foo"""), Path("""C:\foo\bar""").parent) + assertEquals(Path("""C:\あいうえお"""), Path("""C:\あいうえお\かきくけこ""").parent) + assertEquals(Path("""C:\一二三四"""), Path("""C:\一二三四\五六七八""").parent) + assertEquals(null, Path("""C:\""").parent) + } -class SmokeFileTestWindowsMinGW { @Test fun uncParent() { - assertEquals(Path("\\\\server"), Path("\\\\server\\share").parent) - assertEquals(Path("\\\\server\\share"), Path("\\\\server\\share\\dir").parent) + assertEquals(Path("""\\server\share"""), Path("""\\server\share\dir""").parent) + // This is a root UNC path, so parent is + assertEquals(null, Path("""\\server\share""").parent) + } + + @Test + fun basename(){ + assertEquals("あいうえお", Path("""C:\あいうえお""").name) + assertEquals("", Path("""C:\""").name) + } + + + @Test + fun isAbs() { + assertEquals(true, Path("""C:\foo""").isAbsolute, """C:\foo""") + assertEquals(false, Path("""foo\bar""").isAbsolute, """foo\bar""") + assertEquals(true, Path("""\foo\bar""").isAbsolute, """\foo\bar""") + assertEquals(true, Path("""C:\""").isAbsolute, """C:\""") + assertEquals(true, Path("""\\server\share\dir""").isAbsolute, """\\server\share\dir""") + } + + @Test + fun testFormatError() { + val s = formatWin32ErrorMessage(ERROR_TOO_MANY_OPEN_FILES.toUInt()) + // it should be trimmed, drop the trailing rubbish + assertEquals(s.trim(), s.trim()) + } + + @Test + fun testReadDir() { + val expected = listOf("foo", "いろは歌", "天地玄黄") + val actual = SystemFileSystem.list(Path("""./mingw/testdir""")).map { it.name }.sorted() + assertEquals(expected, actual) } } diff --git a/core/mingw/testdir/foo b/core/mingw/testdir/foo new file mode 100644 index 000000000..e69de29bb diff --git "a/core/mingw/testdir/\343\201\204\343\202\215\343\201\257\346\255\214" "b/core/mingw/testdir/\343\201\204\343\202\215\343\201\257\346\255\214" new file mode 100644 index 000000000..e69de29bb diff --git "a/core/mingw/testdir/\345\244\251\345\234\260\347\216\204\351\273\204" "b/core/mingw/testdir/\345\244\251\345\234\260\347\216\204\351\273\204" new file mode 100644 index 000000000..e69de29bb From a34b703b2be6d2dea2e68f902ed49fabe7f348dc Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 02:45:27 +0800 Subject: [PATCH 03/16] WIP --- .../kotlinx-io-multiplatform.gradle.kts | 4 ++ core/apple/src/files/FileSystemApple.kt | 30 ------------- .../src/files/FileSystemLinux.kt | 44 +++++++++++++++++++ core/linux/src/files/FileSystemLinux.kt | 33 -------------- 4 files changed, 48 insertions(+), 63 deletions(-) create mode 100644 core/appleAndLinux/src/files/FileSystemLinux.kt diff --git a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts index 26a7e0cf5..e42e4b4f4 100644 --- a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts +++ b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts @@ -97,6 +97,10 @@ kotlin { group("androidNative") } } + group("appleAndLinux") { + group("apple") + group("linux") + } } group("nodeFilesystemShared") { withJs() diff --git a/core/apple/src/files/FileSystemApple.kt b/core/apple/src/files/FileSystemApple.kt index e13664ca8..134f09c20 100644 --- a/core/apple/src/files/FileSystemApple.kt +++ b/core/apple/src/files/FileSystemApple.kt @@ -6,10 +6,8 @@ package kotlinx.io.files -import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cstr -import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.toKString import kotlinx.io.IOException @@ -19,9 +17,7 @@ import platform.Foundation.NSFileType import platform.Foundation.NSFileTypeDirectory import platform.Foundation.NSFileTypeRegular import platform.Foundation.NSTemporaryDirectory -import platform.posix.DIR import platform.posix.basename -import platform.posix.closedir import platform.posix.dirname import platform.posix.errno import platform.posix.free @@ -84,29 +80,3 @@ internal actual fun metadataOrNullImpl(path: Path): FileMetadata? { ) } -@OptIn(ExperimentalForeignApi::class) -internal actual class OpaqueDirEntry(private val dir: CPointer) : AutoCloseable { - actual fun readdir(): String? { - val entry = platform.posix.readdir(dir) ?: return null - return entry[0].d_name.toKString() - } - - actual override fun close() { - if (closedir(dir) != 0) { - val err = errno - val strerr = strerror(err)?.toKString() ?: "unknown error" - throw IOException("closedir failed with errno $err ($strerr)") - } - } -} - -@OptIn(ExperimentalForeignApi::class) -internal actual fun opendir(path: String): OpaqueDirEntry { - val dirent = platform.posix.opendir(path) - if (dirent != null) return OpaqueDirEntry(dirent) - - val err = errno - val strerr = strerror(err)?.toKString() ?: "unknown error" - throw IOException("Can't open directory $path: $err ($strerr)") -} - diff --git a/core/appleAndLinux/src/files/FileSystemLinux.kt b/core/appleAndLinux/src/files/FileSystemLinux.kt new file mode 100644 index 000000000..3d4914f5f --- /dev/null +++ b/core/appleAndLinux/src/files/FileSystemLinux.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package kotlinx.io.files + +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.get +import kotlinx.cinterop.toKString +import kotlinx.io.IOException +import platform.posix.DIR +import platform.posix.closedir +import platform.posix.errno +import platform.posix.opendir +import platform.posix.strerror + + +@OptIn(ExperimentalForeignApi::class) +internal actual class OpaqueDirEntry(private val dir: CPointer) : AutoCloseable { + actual fun readdir(): String? { + val entry = platform.posix.readdir(dir) ?: return null + return entry[0].d_name.toKString() + } + + actual override fun close() { + if (closedir(dir) != 0) { + val err = errno + val strerr = strerror(err)?.toKString() ?: "unknown error" + throw IOException("closedir failed with errno $err ($strerr)") + } + } +} + +@OptIn(ExperimentalForeignApi::class) +internal actual fun opendir(path: String): OpaqueDirEntry { + val dirent = opendir(path) + if (dirent != null) return OpaqueDirEntry(dirent) + + val err = errno + val strerr = strerror(err)?.toKString() ?: "unknown error" + throw IOException("Can't open directory $path: $err ($strerr)") +} diff --git a/core/linux/src/files/FileSystemLinux.kt b/core/linux/src/files/FileSystemLinux.kt index 00ca4330c..c072d570a 100644 --- a/core/linux/src/files/FileSystemLinux.kt +++ b/core/linux/src/files/FileSystemLinux.kt @@ -5,19 +5,12 @@ package kotlinx.io.files -import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cstr -import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.toKString -import kotlinx.io.IOException -import platform.posix.DIR import platform.posix.__xpg_basename -import platform.posix.closedir import platform.posix.dirname -import platform.posix.errno -import platform.posix.strerror @OptIn(ExperimentalForeignApi::class) internal actual fun dirnameImpl(path: String): String { @@ -38,29 +31,3 @@ internal actual fun basenameImpl(path: String): String { internal actual fun isAbsoluteImpl(path: String): Boolean = path.startsWith('/') - -@OptIn(ExperimentalForeignApi::class) -internal actual class OpaqueDirEntry(private val dir: CPointer) : AutoCloseable { - actual fun readdir(): String? { - val entry = platform.posix.readdir(dir) ?: return null - return entry[0].d_name.toKString() - } - - actual override fun close() { - if (closedir(dir) != 0) { - val err = errno - val strerr = strerror(err)?.toKString() ?: "unknown error" - throw IOException("closedir failed with errno $err ($strerr)") - } - } -} - -@OptIn(ExperimentalForeignApi::class) -internal actual fun opendir(path: String): OpaqueDirEntry { - val dirent = platform.posix.opendir(path) - if (dirent != null) return OpaqueDirEntry(dirent) - - val err = errno - val strerr = strerror(err)?.toKString() ?: "unknown error" - throw IOException("Can't open directory $path: $err ($strerr)") -} From 4f006e5dc4cda19df04f992db87f5def7d686906 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 03:06:58 +0800 Subject: [PATCH 04/16] coopyright --- core/appleAndLinux/src/files/FileSystemLinux.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/appleAndLinux/src/files/FileSystemLinux.kt b/core/appleAndLinux/src/files/FileSystemLinux.kt index 3d4914f5f..86e051ae8 100644 --- a/core/appleAndLinux/src/files/FileSystemLinux.kt +++ b/core/appleAndLinux/src/files/FileSystemLinux.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. */ From ef00a2607452e8b07eb65db429bc42d7381dd93c Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 03:11:36 +0800 Subject: [PATCH 05/16] remove redundant newline --- core/linux/src/files/FileSystemLinux.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/linux/src/files/FileSystemLinux.kt b/core/linux/src/files/FileSystemLinux.kt index c072d570a..5c46e71f4 100644 --- a/core/linux/src/files/FileSystemLinux.kt +++ b/core/linux/src/files/FileSystemLinux.kt @@ -30,4 +30,3 @@ internal actual fun basenameImpl(path: String): String { } internal actual fun isAbsoluteImpl(path: String): Boolean = path.startsWith('/') - From 617957ca2faed886dc080015dd603806115f85ac Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 03:17:04 +0800 Subject: [PATCH 06/16] fix --- core/mingw/src/files/FileSystemMingw.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index f6b45c680..8d9c1e702 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -55,12 +55,12 @@ private typealias PathCchRemoveFileSpecFunc = CPointer getProcAddressOrFailed(module: HMODULE, name: String): CPointer { - val pointer = GetProcAddress(kernelBaseDll, "PathCchRemoveFileSpec") ?: terminateWithUnhandledException( + val pointer = GetProcAddress(kernelBaseDll, name) ?: terminateWithUnhandledException( UnsupportedOperationException("Failed to get proc: $name: ${formatWin32ErrorMessage()}"), ) return pointer.reinterpret() From b761de0c122f4dd33448811290af20850157408a Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 04:36:07 +0800 Subject: [PATCH 07/16] fix --- .../kotlinx-io-multiplatform.gradle.kts | 4 ++ core/mingw/src/files/FileSystemMingw.kt | 59 ++++++------------- .../test/files/SmokeFileTestWindowsMinGW.kt | 11 +++- core/native/src/files/FileSystemNative.kt | 15 ++--- .../kotlin/files/FileSystemNative.posix.kt | 24 ++++++++ 5 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 core/src/posixMain/kotlin/files/FileSystemNative.posix.kt diff --git a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts index e42e4b4f4..2e26a7749 100644 --- a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts +++ b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts @@ -101,6 +101,10 @@ kotlin { group("apple") group("linux") } + group("posix") { + group("apple") + group("unix") + } } group("nodeFilesystemShared") { withJs() diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index 8d9c1e702..3392bb28c 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -7,48 +7,10 @@ package kotlinx.io.files -import kotlinx.cinterop.Arena -import kotlinx.cinterop.CFunction -import kotlinx.cinterop.CPointed -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.allocArray -import kotlinx.cinterop.convert -import kotlinx.cinterop.invoke -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.toKString -import kotlinx.cinterop.wcstr +import kotlinx.cinterop.* import kotlinx.io.IOException import platform.posix.size_t -import platform.windows.CloseHandle -import platform.windows.CreateDirectoryW -import platform.windows.ERROR_FILE_NOT_FOUND -import platform.windows.ERROR_NO_MORE_FILES -import platform.windows.FALSE -import platform.windows.FindClose -import platform.windows.FindFirstFileW -import platform.windows.FindNextFileW -import platform.windows.GetFullPathNameW -import platform.windows.GetLastError -import platform.windows.GetProcAddress -import platform.windows.HANDLE -import platform.windows.HMODULE -import platform.windows.HRESULT -import platform.windows.INVALID_HANDLE_VALUE -import platform.windows.LoadLibraryW -import platform.windows.MAX_PATH -import platform.windows.MOVEFILE_REPLACE_EXISTING -import platform.windows.MoveFileExW -import platform.windows.PWSTR -import platform.windows.PathFindFileNameW -import platform.windows.PathIsRelativeW -import platform.windows.PathIsRootW -import platform.windows.TRUE -import platform.windows.WCHARVar -import platform.windows.WIN32_FIND_DATAW +import platform.windows.* import kotlin.experimental.ExperimentalNativeApi private typealias PathCchRemoveFileSpecFunc = CPointer HRESULT>> @@ -86,7 +48,7 @@ internal actual fun dirnameImpl(path: String): String { } internal actual fun basenameImpl(path: String): String { - if (PathIsRootW(path) == TRUE) return "" + if (PathIsRootW(path) == TRUE) return "" return PathFindFileNameW(path)?.toKString() ?: "" } @@ -179,3 +141,18 @@ internal actual class OpaqueDirEntry(private val directory: String) : AutoClosea } internal actual fun opendir(path: String): OpaqueDirEntry = OpaqueDirEntry(path) + +internal actual fun existsImpl(path: String): Boolean = PathFileExistsW(path) == TRUE + +internal actual fun deleteNoCheckImpl(path: String) { + if (DeleteFileW(path) != FALSE) return + var e = GetLastError() + if (e == ERROR_FILE_NOT_FOUND.toUInt()) return // ignore it + if (e == ERROR_ACCESS_DENIED.toUInt()) { + // might be a directory + if (RemoveDirectoryW(path) != FALSE) return + e = GetLastError() + if (e == ERROR_FILE_NOT_FOUND.toUInt()) return // ignore it + } + throw IOException("Delete failed for $path: ${formatWin32ErrorMessage(e)}") +} diff --git a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt index c6c9debe5..8dc164809 100644 --- a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt +++ b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt @@ -13,8 +13,10 @@ import platform.windows.ERROR_TOO_MANY_OPEN_FILES import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails +import kotlin.test.assertTrue class SmokeFileTestWindowsMinGW { + private val testDir = Path("""./mingw/testdir""") @OptIn(ExperimentalForeignApi::class) @Test fun mingwProblem() { @@ -74,7 +76,14 @@ class SmokeFileTestWindowsMinGW { @Test fun testReadDir() { val expected = listOf("foo", "いろは歌", "天地玄黄") - val actual = SystemFileSystem.list(Path("""./mingw/testdir""")).map { it.name }.sorted() + val actual = SystemFileSystem.list(testDir).map { it.name }.sorted() assertEquals(expected, actual) } + + @Test + fun testExists() { + for (path in SystemFileSystem.list(testDir)) { + assertTrue(SystemFileSystem.exists(path), path.toString()) + } + } } diff --git a/core/native/src/files/FileSystemNative.kt b/core/native/src/files/FileSystemNative.kt index 1fa719b15..9859d7efa 100644 --- a/core/native/src/files/FileSystemNative.kt +++ b/core/native/src/files/FileSystemNative.kt @@ -16,9 +16,7 @@ import kotlin.experimental.ExperimentalNativeApi @OptIn(ExperimentalForeignApi::class) public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() { - override fun exists(path: Path): Boolean { - return access(path.path, F_OK) == 0 - } + override fun exists(path: Path): Boolean = existsImpl(path.path) @OptIn(ExperimentalForeignApi::class) override fun delete(path: Path, mustExist: Boolean) { @@ -28,12 +26,7 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() } return } - if (remove(path.path) != 0) { - if (errno == EACCES) { - if (rmdir(path.path) == 0) return - } - throw IOException("Delete failed for $path: ${strerror(errno)?.toKString()}") - } + deleteNoCheckImpl(path.path) } override fun createDirectories(path: Path, mustCreate: Boolean) { @@ -114,6 +107,10 @@ internal expect fun mkdirImpl(path: String) internal expect fun realpathImpl(path: String): String +internal expect fun existsImpl(path: String): Boolean + +internal expect fun deleteNoCheckImpl(path: String) + public actual open class FileNotFoundException actual constructor( message: String? ) : IOException(message) diff --git a/core/src/posixMain/kotlin/files/FileSystemNative.posix.kt b/core/src/posixMain/kotlin/files/FileSystemNative.posix.kt new file mode 100644 index 000000000..fd69a9358 --- /dev/null +++ b/core/src/posixMain/kotlin/files/FileSystemNative.posix.kt @@ -0,0 +1,24 @@ +package kotlinx.io.files + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import kotlinx.io.IOException +import platform.posix.EACCES +import platform.posix.F_OK +import platform.posix.access +import platform.posix.errno +import platform.posix.remove +import platform.posix.rmdir +import platform.posix.strerror + +internal actual fun existsImpl(path: String): Boolean = access(path, F_OK) == 0 + +@OptIn(ExperimentalForeignApi::class) +internal actual fun deleteNoCheckImpl(path: String) { + if (remove(path) != 0) { + if (errno == EACCES) { + if (rmdir(path) == 0) return + } + throw IOException("Delete failed for $path: ${strerror(errno)?.toKString()}") + } +} From 6bad425ca4dda25a35f62c68e4790dc40515d31d Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 04:55:14 +0800 Subject: [PATCH 08/16] fix --- core/mingw/src/files/FileSystemMingw.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index 3392bb28c..3b24e69f6 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -39,6 +39,7 @@ internal actual fun atomicMoveImpl(source: Path, destination: Path) { } internal actual fun dirnameImpl(path: String): String { + val path = path.replace(UnixPathSeparator, WindowsPathSeparator) memScoped { val p = path.wcstr.ptr // we don't care the result, even it failed. From d62725910308047ea2fb03fa559c5a1352fdc5f2 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 06:30:52 +0800 Subject: [PATCH 09/16] fix --- core/apple/src/files/FileSystemApple.kt | 1 + core/common/src/files/Paths.kt | 4 ++-- core/common/test/files/SmokeFileTest.kt | 2 +- core/mingw/src/files/PathsMingw.kt | 3 +++ core/native/src/files/PathsNative.kt | 1 - .../src/files/FileSystemNativePosix.kt} | 0 core/posix/src/files/PathsPosix.kt | 3 +++ 7 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 core/mingw/src/files/PathsMingw.kt rename core/{src/posixMain/kotlin/files/FileSystemNative.posix.kt => posix/src/files/FileSystemNativePosix.kt} (100%) create mode 100644 core/posix/src/files/PathsPosix.kt diff --git a/core/apple/src/files/FileSystemApple.kt b/core/apple/src/files/FileSystemApple.kt index 134f09c20..2c54e9e2e 100644 --- a/core/apple/src/files/FileSystemApple.kt +++ b/core/apple/src/files/FileSystemApple.kt @@ -6,6 +6,7 @@ package kotlinx.io.files +import files.SystemPathSeparator import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cstr import kotlinx.cinterop.memScoped diff --git a/core/common/src/files/Paths.kt b/core/common/src/files/Paths.kt index a2faedfa1..10907512c 100644 --- a/core/common/src/files/Paths.kt +++ b/core/common/src/files/Paths.kt @@ -75,7 +75,7 @@ public expect val SystemPathSeparator: Char public expect fun Path(path: String): Path /** - * Returns Path for the given [base] path concatenated with [parts] using [SystemPathSeparator]. + * Returns Path for the given [base] path concatenated with [parts] using [files.SystemPathSeparator]. */ public fun Path(base: String, vararg parts: String): Path { // Parameter name has to be specified explicitly to overcome https://youtrack.jetbrains.com/issue/KT-22520 @@ -91,7 +91,7 @@ public fun Path(base: String, vararg parts: String): Path { } /** - * Returns Path for the given [base] path concatenated with [parts] using [SystemPathSeparator]. + * Returns Path for the given [base] path concatenated with [parts] using [files.SystemPathSeparator]. */ public fun Path(base: Path, vararg parts: String): Path { return Path(base.toString(), *parts) diff --git a/core/common/test/files/SmokeFileTest.kt b/core/common/test/files/SmokeFileTest.kt index 7761b4566..fbf59a26a 100644 --- a/core/common/test/files/SmokeFileTest.kt +++ b/core/common/test/files/SmokeFileTest.kt @@ -186,7 +186,7 @@ class SmokeFileTest { @Test fun pathParent() { val p = Path(SystemPathSeparator.toString(), "a", "b", "c") - assertEquals(constructAbsolutePath("a", "b"), p.parent?.toString()) + assertEquals(constructAbsolutePath("a", "b").also { println(it) }, p.parent?.toString()) assertEquals(constructAbsolutePath("a"), p.parent?.parent?.toString()) assertEquals(constructAbsolutePath(), p.parent?.parent?.parent?.toString()) assertNull(p.parent?.parent?.parent?.parent) diff --git a/core/mingw/src/files/PathsMingw.kt b/core/mingw/src/files/PathsMingw.kt new file mode 100644 index 000000000..f09f1cec3 --- /dev/null +++ b/core/mingw/src/files/PathsMingw.kt @@ -0,0 +1,3 @@ +package kotlinx.io.files + +public actual val SystemPathSeparator: Char get() = WindowsPathSeparator diff --git a/core/native/src/files/PathsNative.kt b/core/native/src/files/PathsNative.kt index e013afd60..85ed8bbc4 100644 --- a/core/native/src/files/PathsNative.kt +++ b/core/native/src/files/PathsNative.kt @@ -54,7 +54,6 @@ public actual class Path internal constructor( } } -public actual val SystemPathSeparator: Char get() = UnixPathSeparator internal expect fun dirnameImpl(path: String): String diff --git a/core/src/posixMain/kotlin/files/FileSystemNative.posix.kt b/core/posix/src/files/FileSystemNativePosix.kt similarity index 100% rename from core/src/posixMain/kotlin/files/FileSystemNative.posix.kt rename to core/posix/src/files/FileSystemNativePosix.kt diff --git a/core/posix/src/files/PathsPosix.kt b/core/posix/src/files/PathsPosix.kt new file mode 100644 index 000000000..c88f4696b --- /dev/null +++ b/core/posix/src/files/PathsPosix.kt @@ -0,0 +1,3 @@ +package kotlinx.io.files + +public actual val SystemPathSeparator: Char get() = UnixPathSeparator From 3c42feed678cdb967c333229d900b527f61324d2 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 06:35:34 +0800 Subject: [PATCH 10/16] fix --- core/apple/src/files/FileSystemApple.kt | 17 ++--------------- core/mingw/src/files/Error.kt | 16 ++-------------- core/posix/src/files/FileSystemNativePosix.kt | 8 +------- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/core/apple/src/files/FileSystemApple.kt b/core/apple/src/files/FileSystemApple.kt index 2c54e9e2e..9fd43a0e1 100644 --- a/core/apple/src/files/FileSystemApple.kt +++ b/core/apple/src/files/FileSystemApple.kt @@ -6,26 +6,13 @@ package kotlinx.io.files -import files.SystemPathSeparator import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cstr import kotlinx.cinterop.memScoped import kotlinx.cinterop.toKString import kotlinx.io.IOException -import platform.Foundation.NSFileManager -import platform.Foundation.NSFileSize -import platform.Foundation.NSFileType -import platform.Foundation.NSFileTypeDirectory -import platform.Foundation.NSFileTypeRegular -import platform.Foundation.NSTemporaryDirectory -import platform.posix.basename -import platform.posix.dirname -import platform.posix.errno -import platform.posix.free -import platform.posix.mkdir -import platform.posix.realpath -import platform.posix.rename -import platform.posix.strerror +import platform.Foundation.* +import platform.posix.* internal actual fun atomicMoveImpl(source: Path, destination: Path) { diff --git a/core/mingw/src/files/Error.kt b/core/mingw/src/files/Error.kt index 19a3048f9..f20fd9b0f 100644 --- a/core/mingw/src/files/Error.kt +++ b/core/mingw/src/files/Error.kt @@ -1,19 +1,7 @@ package kotlinx.io.files -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.toKString -import kotlinx.cinterop.value -import platform.windows.FORMAT_MESSAGE_ALLOCATE_BUFFER -import platform.windows.FORMAT_MESSAGE_FROM_SYSTEM -import platform.windows.FORMAT_MESSAGE_IGNORE_INSERTS -import platform.windows.FormatMessageW -import platform.windows.GetLastError -import platform.windows.LPWSTRVar -import platform.windows.LocalFree +import kotlinx.cinterop.* +import platform.windows.* @OptIn(ExperimentalForeignApi::class) internal fun formatWin32ErrorMessage(code: UInt = GetLastError()): String { diff --git a/core/posix/src/files/FileSystemNativePosix.kt b/core/posix/src/files/FileSystemNativePosix.kt index fd69a9358..3f2362669 100644 --- a/core/posix/src/files/FileSystemNativePosix.kt +++ b/core/posix/src/files/FileSystemNativePosix.kt @@ -3,13 +3,7 @@ package kotlinx.io.files import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.toKString import kotlinx.io.IOException -import platform.posix.EACCES -import platform.posix.F_OK -import platform.posix.access -import platform.posix.errno -import platform.posix.remove -import platform.posix.rmdir -import platform.posix.strerror +import platform.posix.* internal actual fun existsImpl(path: String): Boolean = access(path, F_OK) == 0 From 005b623377c638cd51aafa7a5d17bb24348fcf46 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 06:38:40 +0800 Subject: [PATCH 11/16] fix --- core/apple/src/files/FileSystemApple.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/apple/src/files/FileSystemApple.kt b/core/apple/src/files/FileSystemApple.kt index 9fd43a0e1..75882b967 100644 --- a/core/apple/src/files/FileSystemApple.kt +++ b/core/apple/src/files/FileSystemApple.kt @@ -6,10 +6,7 @@ package kotlinx.io.files -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.cstr -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.toKString +import kotlinx.cinterop.* import kotlinx.io.IOException import platform.Foundation.* import platform.posix.* @@ -67,4 +64,3 @@ internal actual fun metadataOrNullImpl(path: Path): FileMetadata? { size = if (isFile) attributes[NSFileSize] as Long else -1 ) } - From 801e82345ba3ae74cbc21c23eb3a8106dcbdd8c1 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 06:40:54 +0800 Subject: [PATCH 12/16] fix --- core/common/src/files/Paths.kt | 4 ++-- core/common/test/files/SmokeFileTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/src/files/Paths.kt b/core/common/src/files/Paths.kt index 10907512c..a2faedfa1 100644 --- a/core/common/src/files/Paths.kt +++ b/core/common/src/files/Paths.kt @@ -75,7 +75,7 @@ public expect val SystemPathSeparator: Char public expect fun Path(path: String): Path /** - * Returns Path for the given [base] path concatenated with [parts] using [files.SystemPathSeparator]. + * Returns Path for the given [base] path concatenated with [parts] using [SystemPathSeparator]. */ public fun Path(base: String, vararg parts: String): Path { // Parameter name has to be specified explicitly to overcome https://youtrack.jetbrains.com/issue/KT-22520 @@ -91,7 +91,7 @@ public fun Path(base: String, vararg parts: String): Path { } /** - * Returns Path for the given [base] path concatenated with [parts] using [files.SystemPathSeparator]. + * Returns Path for the given [base] path concatenated with [parts] using [SystemPathSeparator]. */ public fun Path(base: Path, vararg parts: String): Path { return Path(base.toString(), *parts) diff --git a/core/common/test/files/SmokeFileTest.kt b/core/common/test/files/SmokeFileTest.kt index fbf59a26a..7761b4566 100644 --- a/core/common/test/files/SmokeFileTest.kt +++ b/core/common/test/files/SmokeFileTest.kt @@ -186,7 +186,7 @@ class SmokeFileTest { @Test fun pathParent() { val p = Path(SystemPathSeparator.toString(), "a", "b", "c") - assertEquals(constructAbsolutePath("a", "b").also { println(it) }, p.parent?.toString()) + assertEquals(constructAbsolutePath("a", "b"), p.parent?.toString()) assertEquals(constructAbsolutePath("a"), p.parent?.parent?.toString()) assertEquals(constructAbsolutePath(), p.parent?.parent?.parent?.toString()) assertNull(p.parent?.parent?.parent?.parent) From 4d7d065834143fa11e885cf2729577a799567080 Mon Sep 17 00:00:00 2001 From: iseki Date: Tue, 30 Sep 2025 07:09:52 +0800 Subject: [PATCH 13/16] fix --- core/common/test/files/SmokeFileTestWindows.kt | 4 ++-- core/mingw/src/files/FileSystemMingw.kt | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/core/common/test/files/SmokeFileTestWindows.kt b/core/common/test/files/SmokeFileTestWindows.kt index 23994807e..58c802883 100644 --- a/core/common/test/files/SmokeFileTestWindows.kt +++ b/core/common/test/files/SmokeFileTestWindows.kt @@ -11,11 +11,11 @@ class SmokeFileTestWindows { @Test fun isAbsolute() { if (!isWindows) return - assertFalse(Path("C:").isAbsolute) + assertTrue(Path("C:").isAbsolute) assertTrue(Path("C:\\").isAbsolute) assertTrue(Path("C:/").isAbsolute) assertTrue(Path("C:/../").isAbsolute) - assertFalse(Path("C:file").isAbsolute) + assertTrue(Path("C:file").isAbsolute) assertFalse(Path("bla\\bla\\bla").isAbsolute) assertTrue(Path("\\\\server\\share").isAbsolute) } diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index 3b24e69f6..1924ec25c 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -54,12 +54,6 @@ internal actual fun basenameImpl(path: String): String { } internal actual fun isAbsoluteImpl(path: String): Boolean { - if (path.startsWith(SystemPathSeparator)) return true - if (path.length > 1 && path[1] == ':') { - if (path.length == 2) return false - val next = path[2] - return next == WindowsPathSeparator || next == SystemPathSeparator - } return PathIsRelativeW(path) == 0 } From d45c75d1dc5d004ebac8050c1a9fe0c2d8b0e578 Mon Sep 17 00:00:00 2001 From: iseki Date: Thu, 9 Oct 2025 20:38:39 +0800 Subject: [PATCH 14/16] reformat --- core/mingw/test/files/SmokeFileTestWindowsMinGW.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt index 8dc164809..efc48f2d3 100644 --- a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt +++ b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt @@ -17,6 +17,7 @@ import kotlin.test.assertTrue class SmokeFileTestWindowsMinGW { private val testDir = Path("""./mingw/testdir""") + @OptIn(ExperimentalForeignApi::class) @Test fun mingwProblem() { @@ -51,7 +52,7 @@ class SmokeFileTestWindowsMinGW { } @Test - fun basename(){ + fun basename() { assertEquals("あいうえお", Path("""C:\あいうえお""").name) assertEquals("", Path("""C:\""").name) } From 98cdacd069158bb3bd1fdbd4b90ecda42b1607c2 Mon Sep 17 00:00:00 2001 From: iseki Date: Fri, 10 Oct 2025 14:58:05 +0800 Subject: [PATCH 15/16] WIP --- .../common/test/files/SmokeFileTestWindows.kt | 21 +++++++---- core/mingw/src/files/Error.kt | 4 +++ core/mingw/src/files/FileSystemMingw.kt | 35 +++++++------------ core/mingw/src/files/PathsMingw.kt | 4 +++ .../test/files/SmokeFileTestWindowsMinGW.kt | 23 ++++++------ core/posix/src/files/FileSystemNativePosix.kt | 4 +++ core/posix/src/files/PathsPosix.kt | 4 +++ 7 files changed, 52 insertions(+), 43 deletions(-) diff --git a/core/common/test/files/SmokeFileTestWindows.kt b/core/common/test/files/SmokeFileTestWindows.kt index 58c802883..ecf7096a5 100644 --- a/core/common/test/files/SmokeFileTestWindows.kt +++ b/core/common/test/files/SmokeFileTestWindows.kt @@ -11,13 +11,20 @@ class SmokeFileTestWindows { @Test fun isAbsolute() { if (!isWindows) return - assertTrue(Path("C:").isAbsolute) - assertTrue(Path("C:\\").isAbsolute) - assertTrue(Path("C:/").isAbsolute) - assertTrue(Path("C:/../").isAbsolute) - assertTrue(Path("C:file").isAbsolute) - assertFalse(Path("bla\\bla\\bla").isAbsolute) - assertTrue(Path("\\\\server\\share").isAbsolute) + fun assertIsAbsolute(path: String) { + assertTrue(Path(path).isAbsolute, "Expected absolute path: $path") + } + fun assertIsRelative(path: String) { + assertFalse(Path(path).isAbsolute, "Expected relative path: $path") + } + assertIsRelative("C:") + assertIsAbsolute("C:\\") + assertIsAbsolute("C:/") + assertIsAbsolute("C:/../") + assertIsRelative("C:file") + assertIsRelative("bla\\bla\\bla") + assertIsAbsolute("\\\\server\\share") + assertIsAbsolute("\\\\?\\C:\\Test\\Foo.txt") } @Test diff --git a/core/mingw/src/files/Error.kt b/core/mingw/src/files/Error.kt index f20fd9b0f..459563b13 100644 --- a/core/mingw/src/files/Error.kt +++ b/core/mingw/src/files/Error.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ package kotlinx.io.files import kotlinx.cinterop.* diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index 1924ec25c..525c6f057 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -9,28 +9,8 @@ package kotlinx.io.files import kotlinx.cinterop.* import kotlinx.io.IOException -import platform.posix.size_t import platform.windows.* -import kotlin.experimental.ExperimentalNativeApi -private typealias PathCchRemoveFileSpecFunc = CPointer HRESULT>> - -@OptIn(ExperimentalNativeApi::class) -private val kernelBaseDll = LoadLibraryW("kernelbase.dll") ?: run { - terminateWithUnhandledException(RuntimeException("kernelbase.dll is not supported: ${formatWin32ErrorMessage()}")) -} - -@OptIn(ExperimentalNativeApi::class) -private fun getProcAddressOrFailed(module: HMODULE, name: String): CPointer { - val pointer = GetProcAddress(kernelBaseDll, name) ?: terminateWithUnhandledException( - UnsupportedOperationException("Failed to get proc: $name: ${formatWin32ErrorMessage()}"), - ) - return pointer.reinterpret() -} - -// Available since Windows 8 / Windows Server 2012, long path and UNC path supported -private val PathCchRemoveFileSpec: PathCchRemoveFileSpecFunc = - getProcAddressOrFailed(kernelBaseDll, "PathCchRemoveFileSpec") internal actual fun atomicMoveImpl(source: Path, destination: Path) { if (MoveFileExW(source.path, destination.path, MOVEFILE_REPLACE_EXISTING.convert()) == 0) { @@ -42,8 +22,9 @@ internal actual fun dirnameImpl(path: String): String { val path = path.replace(UnixPathSeparator, WindowsPathSeparator) memScoped { val p = path.wcstr.ptr - // we don't care the result, even it failed. - PathCchRemoveFileSpec.invoke(p, path.length.convert()) + // This function is deprecated, should use PathCchRemoveFileSpec, + // but it's not available in current version of Kotlin + PathRemoveFileSpecW(p) return p.toKString() } } @@ -54,7 +35,15 @@ internal actual fun basenameImpl(path: String): String { } internal actual fun isAbsoluteImpl(path: String): Boolean { - return PathIsRelativeW(path) == 0 + val p = path.replace(UnixPathSeparator, WindowsPathSeparator) + if (PathIsRelativeW(p) == TRUE) { + return false + } + // PathIsRelativeW returns FALSE for paths like "C:relative\path" which are not absolute, in DoS + if (p.length >= 2 && p[0].isLetter() && p[1] == ':') { + return p.length > 2 && (p[2] == WindowsPathSeparator || p[2] == UnixPathSeparator) + } + return true } internal actual fun mkdirImpl(path: String) { diff --git a/core/mingw/src/files/PathsMingw.kt b/core/mingw/src/files/PathsMingw.kt index f09f1cec3..8329c1d1b 100644 --- a/core/mingw/src/files/PathsMingw.kt +++ b/core/mingw/src/files/PathsMingw.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ package kotlinx.io.files public actual val SystemPathSeparator: Char get() = WindowsPathSeparator diff --git a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt index efc48f2d3..38a5d48c0 100644 --- a/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt +++ b/core/mingw/test/files/SmokeFileTestWindowsMinGW.kt @@ -10,30 +10,37 @@ import kotlinx.cinterop.cstr import kotlinx.cinterop.toKString import platform.posix.dirname import platform.windows.ERROR_TOO_MANY_OPEN_FILES +import platform.windows.GetConsoleCP import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertTrue +@OptIn(ExperimentalForeignApi::class) class SmokeFileTestWindowsMinGW { private val testDir = Path("""./mingw/testdir""") @OptIn(ExperimentalForeignApi::class) @Test fun mingwProblem() { + // Skipping test because console code page is UTF-8, + // use when clause because I'm not sure which codepage should be skipped + when (GetConsoleCP()) { + 65001u -> return + } assertEquals("""C:\foo""", dirname("""C:\foo\bar""".cstr)!!.toKString()) assertFails { assertEquals( """C:\あいうえお""", dirname("""C:\あいうえお\かきくけこ""".cstr)!!.toKString(), ) - }.let(::println) + } assertFails { assertEquals( """C:\一二三四""", dirname("""C:\一二三四\五六七八""".cstr)!!.toKString(), ) - }.let(::println) + } } @Test @@ -57,21 +64,11 @@ class SmokeFileTestWindowsMinGW { assertEquals("", Path("""C:\""").name) } - - @Test - fun isAbs() { - assertEquals(true, Path("""C:\foo""").isAbsolute, """C:\foo""") - assertEquals(false, Path("""foo\bar""").isAbsolute, """foo\bar""") - assertEquals(true, Path("""\foo\bar""").isAbsolute, """\foo\bar""") - assertEquals(true, Path("""C:\""").isAbsolute, """C:\""") - assertEquals(true, Path("""\\server\share\dir""").isAbsolute, """\\server\share\dir""") - } - @Test fun testFormatError() { val s = formatWin32ErrorMessage(ERROR_TOO_MANY_OPEN_FILES.toUInt()) // it should be trimmed, drop the trailing rubbish - assertEquals(s.trim(), s.trim()) + assertEquals(s.trim(), s) } @Test diff --git a/core/posix/src/files/FileSystemNativePosix.kt b/core/posix/src/files/FileSystemNativePosix.kt index 3f2362669..80658473e 100644 --- a/core/posix/src/files/FileSystemNativePosix.kt +++ b/core/posix/src/files/FileSystemNativePosix.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ package kotlinx.io.files import kotlinx.cinterop.ExperimentalForeignApi diff --git a/core/posix/src/files/PathsPosix.kt b/core/posix/src/files/PathsPosix.kt index c88f4696b..784c00e47 100644 --- a/core/posix/src/files/PathsPosix.kt +++ b/core/posix/src/files/PathsPosix.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ package kotlinx.io.files public actual val SystemPathSeparator: Char get() = UnixPathSeparator From 7d5f94ffebaff39d7e3e3f8757cc77d324c74f9f Mon Sep 17 00:00:00 2001 From: iseki Date: Fri, 10 Oct 2025 15:16:48 +0800 Subject: [PATCH 16/16] WIP --- .../src/files/{FileSystemLinux.kt => FileSystemAppleAndLinux.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/appleAndLinux/src/files/{FileSystemLinux.kt => FileSystemAppleAndLinux.kt} (100%) diff --git a/core/appleAndLinux/src/files/FileSystemLinux.kt b/core/appleAndLinux/src/files/FileSystemAppleAndLinux.kt similarity index 100% rename from core/appleAndLinux/src/files/FileSystemLinux.kt rename to core/appleAndLinux/src/files/FileSystemAppleAndLinux.kt