Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ kotlin {
group("androidNative")
}
}

group("nativeNonAndroid") {
group("appleAndLinux") {
group("apple")
group("mingw")
group("linux")
}
group("posix") {
group("apple")
group("unix")
}
}
group("nodeFilesystemShared") {
withJs()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ 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<DIR>) : AutoCloseable {
actual fun readdir(): String? {
Expand All @@ -33,7 +35,7 @@ internal actual class OpaqueDirEntry(private val dir: CPointer<DIR>) : AutoClose

@OptIn(ExperimentalForeignApi::class)
internal actual fun opendir(path: String): OpaqueDirEntry {
val dirent = platform.posix.opendir(path)
val dirent = opendir(path)
if (dirent != null) return OpaqueDirEntry(dirent)

val err = errno
Expand Down
21 changes: 14 additions & 7 deletions core/common/test/files/SmokeFileTestWindows.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ class SmokeFileTestWindows {
@Test
fun isAbsolute() {
if (!isWindows) return
assertFalse(Path("C:").isAbsolute)
assertTrue(Path("C:\\").isAbsolute)
assertTrue(Path("C:/").isAbsolute)
assertTrue(Path("C:/../").isAbsolute)
assertFalse(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
Expand Down
31 changes: 31 additions & 0 deletions core/mingw/src/files/Error.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.*
import platform.windows.*

@OptIn(ExperimentalForeignApi::class)
internal fun formatWin32ErrorMessage(code: UInt = GetLastError()): String {
memScoped {
val r = alloc<LPWSTRVar>()
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) {
return "unknown error (${code.toHexString()})"
}
val s = r.value!!.toKString().trimEnd()
LocalFree(r.value)
return "$s (${code.toHexString()})"
}

}
130 changes: 105 additions & 25 deletions core/mingw/src/files/FileSystemMingw.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,134 @@ package kotlinx.io.files

import kotlinx.cinterop.*
import kotlinx.io.IOException
import platform.posix.*
import platform.windows.*


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 ""
}
val path = path.replace(UnixPathSeparator, WindowsPathSeparator)
memScoped {
return dirname(path.cstr.ptr)?.toKString() ?: ""
val p = path.wcstr.ptr
// This function is deprecated, should use PathCchRemoveFileSpec,
// but it's not available in current version of Kotlin
PathRemoveFileSpecW(p)
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 {
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
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 PathIsRelativeA(path) == 0
return true
}

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<CHARVar>(MAX_PATH_LENGTH)
val len = GetFullPathNameA(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<WCHARVar>(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<WCHARVar>(r.toInt())
r = GetFullPathNameW(path, r, buf, null)
}
if (r == 0u) {
error("GetFullPathNameW failed for $path: ${formatWin32ErrorMessage()}")
}
return buf.toKString()
}
}

internal actual class OpaqueDirEntry(private val directory: String) : AutoCloseable {
private val arena = Arena()
private val data = arena.alloc<WIN32_FIND_DATAW>()
private var handle: HANDLE? = INVALID_HANDLE_VALUE
private var firstName: String? = null

init {
try {
// 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 e = GetLastError()
if (e != ERROR_FILE_NOT_FOUND.toUInt()) {
throw IOException("Can't open directory $directory: ${formatWin32ErrorMessage(e)}")
}
}
} 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
}
throw IOException("Can't readdir from $directory: ${formatWin32ErrorMessage(le)}")
}

actual override fun close() {
if (handle != INVALID_HANDLE_VALUE) {
FindClose(handle)
}
arena.clear()
}

}

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)}")
}
7 changes: 7 additions & 0 deletions core/mingw/src/files/PathsMingw.kt
Original file line number Diff line number Diff line change
@@ -0,0 +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
76 changes: 73 additions & 3 deletions core/mingw/test/files/SmokeFileTestWindowsMinGW.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,83 @@

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 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(),
)
}
assertFails {
assertEquals(
"""C:\一二三四""",
dirname("""C:\一二三四\五六七八""".cstr)!!.toKString(),
)
}
}

@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 testFormatError() {
val s = formatWin32ErrorMessage(ERROR_TOO_MANY_OPEN_FILES.toUInt())
// it should be trimmed, drop the trailing rubbish
assertEquals(s.trim(), s)
}

@Test
fun testReadDir() {
val expected = listOf("foo", "いろは歌", "天地玄黄")
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())
}
}
}
Empty file added core/mingw/testdir/foo
Empty file.
Empty file added core/mingw/testdir/いろは歌
Empty file.
Empty file added core/mingw/testdir/天地玄黄
Empty file.
15 changes: 6 additions & 9 deletions core/native/src/files/FileSystemNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion core/native/src/files/PathsNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ public actual class Path internal constructor(
}
}

public actual val SystemPathSeparator: Char get() = UnixPathSeparator

internal expect fun dirnameImpl(path: String): String

Expand Down
Loading