Skip to content

Commit 3540e98

Browse files
committed
WIP
1 parent eba0b52 commit 3540e98

File tree

6 files changed

+139
-40
lines changed

6 files changed

+139
-40
lines changed

core/mingw/src/files/Error.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import platform.windows.FORMAT_MESSAGE_ALLOCATE_BUFFER
1111
import platform.windows.FORMAT_MESSAGE_FROM_SYSTEM
1212
import platform.windows.FORMAT_MESSAGE_IGNORE_INSERTS
1313
import platform.windows.FormatMessageW
14+
import platform.windows.GetLastError
1415
import platform.windows.LPWSTRVar
1516
import platform.windows.LocalFree
1617

1718
@OptIn(ExperimentalForeignApi::class)
18-
internal fun formatWin32ErrorMessage(code: UInt): String {
19+
internal fun formatWin32ErrorMessage(code: UInt = GetLastError()): String {
1920
memScoped {
2021
val r = alloc<LPWSTRVar>()
2122
val n = FormatMessageW(
@@ -28,11 +29,11 @@ internal fun formatWin32ErrorMessage(code: UInt): String {
2829
Arguments = null,
2930
)
3031
if (n == 0u) {
31-
throw RuntimeException("Error formatting error: $code")
32+
return "unknown error (${code.toHexString()})"
3233
}
33-
val s = r.value?.toKString().orEmpty()
34+
val s = r.value!!.toKString().trimEnd()
3435
LocalFree(r.value)
35-
return s
36+
return "$s (${code.toHexString()})"
3637
}
3738

3839
}

core/mingw/src/files/FileSystemMingw.kt

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,57 +8,86 @@
88
package kotlinx.io.files
99

1010
import kotlinx.cinterop.Arena
11+
import kotlinx.cinterop.CFunction
12+
import kotlinx.cinterop.CPointed
13+
import kotlinx.cinterop.CPointer
1114
import kotlinx.cinterop.ExperimentalForeignApi
1215
import kotlinx.cinterop.alloc
1316
import kotlinx.cinterop.allocArray
1417
import kotlinx.cinterop.convert
15-
import kotlinx.cinterop.cstr
18+
import kotlinx.cinterop.invoke
1619
import kotlinx.cinterop.memScoped
1720
import kotlinx.cinterop.ptr
21+
import kotlinx.cinterop.reinterpret
1822
import kotlinx.cinterop.toKString
23+
import kotlinx.cinterop.wcstr
1924
import kotlinx.io.IOException
20-
import platform.posix.basename
21-
import platform.posix.dirname
22-
import platform.posix.errno
23-
import platform.posix.mkdir
24-
import platform.posix.strerror
25+
import platform.posix.size_t
2526
import platform.windows.CloseHandle
27+
import platform.windows.CreateDirectoryW
2628
import platform.windows.ERROR_FILE_NOT_FOUND
2729
import platform.windows.ERROR_NO_MORE_FILES
30+
import platform.windows.FALSE
2831
import platform.windows.FindClose
2932
import platform.windows.FindFirstFileW
3033
import platform.windows.FindNextFileW
3134
import platform.windows.GetFullPathNameW
3235
import platform.windows.GetLastError
36+
import platform.windows.GetProcAddress
3337
import platform.windows.HANDLE
38+
import platform.windows.HMODULE
39+
import platform.windows.HRESULT
3440
import platform.windows.INVALID_HANDLE_VALUE
41+
import platform.windows.LoadLibraryW
42+
import platform.windows.MAX_PATH
3543
import platform.windows.MOVEFILE_REPLACE_EXISTING
36-
import platform.windows.MoveFileExA
44+
import platform.windows.MoveFileExW
45+
import platform.windows.PWSTR
46+
import platform.windows.PathFindFileNameW
3747
import platform.windows.PathIsRelativeW
48+
import platform.windows.PathIsRootW
3849
import platform.windows.TRUE
3950
import platform.windows.WCHARVar
4051
import platform.windows.WIN32_FIND_DATAW
52+
import kotlin.experimental.ExperimentalNativeApi
53+
54+
private typealias PathCchRemoveFileSpecFunc = CPointer<CFunction<(PWSTR, size_t) -> HRESULT>>
55+
56+
@OptIn(ExperimentalNativeApi::class)
57+
private val kernelBaseDll = LoadLibraryW("kernelbase.dll") ?: run {
58+
terminateWithUnhandledException(RuntimeException("kernelbase_dll is not supported: ${formatWin32ErrorMessage()}"))
59+
}
60+
61+
@OptIn(ExperimentalNativeApi::class)
62+
private fun <T : CPointed> getProcAddressOrFailed(module: HMODULE, name: String): CPointer<T> {
63+
val pointer = GetProcAddress(kernelBaseDll, "PathCchRemoveFileSpec") ?: terminateWithUnhandledException(
64+
UnsupportedOperationException("Failed to get proc: $name: ${formatWin32ErrorMessage()}"),
65+
)
66+
return pointer.reinterpret()
67+
}
68+
69+
// Available since Windows 8 / Windows Server 2012, long path and UNC path supported
70+
private val PathCchRemoveFileSpec: PathCchRemoveFileSpecFunc =
71+
getProcAddressOrFailed(kernelBaseDll, "PathCchRemoveFileSpec")
4172

4273
internal actual fun atomicMoveImpl(source: Path, destination: Path) {
43-
if (MoveFileExA(source.path, destination.path, MOVEFILE_REPLACE_EXISTING.convert()) == 0) {
44-
// TODO: get formatted error message
45-
throw IOException("Move failed with error code: ${GetLastError()}")
74+
if (MoveFileExW(source.path, destination.path, MOVEFILE_REPLACE_EXISTING.convert()) == 0) {
75+
throw IOException("Move failed with error code: ${formatWin32ErrorMessage()}")
4676
}
4777
}
4878

4979
internal actual fun dirnameImpl(path: String): String {
50-
if (!path.contains(UnixPathSeparator) && !path.contains(WindowsPathSeparator)) {
51-
return ""
52-
}
5380
memScoped {
54-
return dirname(path.cstr.ptr)?.toKString() ?: ""
81+
val p = path.wcstr.ptr
82+
// we don't care the result, even it failed.
83+
PathCchRemoveFileSpec.invoke(p, path.length.convert())
84+
return p.toKString()
5585
}
5686
}
5787

5888
internal actual fun basenameImpl(path: String): String {
59-
memScoped {
60-
return basename(path.cstr.ptr)?.toKString() ?: ""
61-
}
89+
if (PathIsRootW(path) == TRUE) return ""
90+
return PathFindFileNameW(path)?.toKString() ?: ""
6291
}
6392

6493
internal actual fun isAbsoluteImpl(path: String): Boolean {
@@ -72,39 +101,46 @@ internal actual fun isAbsoluteImpl(path: String): Boolean {
72101
}
73102

74103
internal actual fun mkdirImpl(path: String) {
75-
if (mkdir(path) != 0) {
76-
throw IOException("mkdir failed: ${strerror(errno)?.toKString()}")
104+
if (CreateDirectoryW(path, null) == FALSE) {
105+
throw IOException("mkdir failed: $path: ${formatWin32ErrorMessage()}")
77106
}
78107
}
79108

80-
private const val MAX_PATH_LENGTH = 32767
81-
82109
internal actual fun realpathImpl(path: String): String {
83110
memScoped {
84-
val buffer = allocArray<WCHARVar>(MAX_PATH_LENGTH)
85-
val len = GetFullPathNameW(path, MAX_PATH_LENGTH.convert(), buffer, null)
86-
if (len == 0u) throw IllegalStateException()
87-
return buffer.toKString()
111+
// in practice, MAX_PATH is enough for most cases
112+
var buf = allocArray<WCHARVar>(MAX_PATH)
113+
var r = GetFullPathNameW(path, MAX_PATH.convert(), buf, null)
114+
if (r >= MAX_PATH.toUInt()) {
115+
// if not, we will retry with the required size
116+
buf = allocArray<WCHARVar>(r.toInt())
117+
r = GetFullPathNameW(path, r, buf, null)
118+
}
119+
if (r == 0u) {
120+
error("GetFullPathNameW failed for $path: ${formatWin32ErrorMessage()}")
121+
}
122+
return buf.toKString()
88123
}
89124
}
90125

91-
internal actual class OpaqueDirEntry(directory: String) : AutoCloseable {
126+
internal actual class OpaqueDirEntry(private val directory: String) : AutoCloseable {
92127
private val arena = Arena()
93128
private val data = arena.alloc<WIN32_FIND_DATAW>()
94129
private var handle: HANDLE? = INVALID_HANDLE_VALUE
95130
private var firstName: String? = null
96131

97132
init {
98133
try {
99-
val directory0 = if (directory.endsWith('/') || directory.endsWith('\\')) "$directory*" else "$directory/*"
134+
// since the root
135+
val directory0 =
136+
if (directory.endsWith(UnixPathSeparator) || directory.endsWith(WindowsPathSeparator)) "$directory*" else "$directory/*"
100137
handle = FindFirstFileW(directory0, data.ptr)
101138
if (handle != INVALID_HANDLE_VALUE) {
102139
firstName = data.cFileName.toKString()
103140
} else {
104-
val le = GetLastError()
105-
if (le != ERROR_FILE_NOT_FOUND.toUInt()) {
106-
val strerr = formatWin32ErrorMessage(le)
107-
throw IOException("Can't open directory $directory: $le ($strerr)")
141+
val e = GetLastError()
142+
if (e != ERROR_FILE_NOT_FOUND.toUInt()) {
143+
throw IOException("Can't open directory $directory: ${formatWin32ErrorMessage(e)}")
108144
}
109145
}
110146
} catch (th: Throwable) {
@@ -130,8 +166,7 @@ internal actual class OpaqueDirEntry(directory: String) : AutoCloseable {
130166
if (le == ERROR_NO_MORE_FILES.toUInt()) {
131167
return null
132168
}
133-
val strerr = formatWin32ErrorMessage(le)
134-
throw IOException("Can't readdir: $le ($strerr)")
169+
throw IOException("Can't readdir from $directory: ${formatWin32ErrorMessage(le)}")
135170
}
136171

137172
actual override fun close() {

core/mingw/test/files/SmokeFileTestWindowsMinGW.kt

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,76 @@
55

66
package kotlinx.io.files
77

8+
import kotlinx.cinterop.ExperimentalForeignApi
9+
import kotlinx.cinterop.cstr
10+
import kotlinx.cinterop.toKString
11+
import platform.posix.dirname
12+
import platform.windows.ERROR_TOO_MANY_OPEN_FILES
813
import kotlin.test.Test
914
import kotlin.test.assertEquals
15+
import kotlin.test.assertFails
16+
17+
class SmokeFileTestWindowsMinGW {
18+
@OptIn(ExperimentalForeignApi::class)
19+
@Test
20+
fun mingwProblem() {
21+
assertEquals("""C:\foo""", dirname("""C:\foo\bar""".cstr)!!.toKString())
22+
assertFails {
23+
assertEquals(
24+
"""C:\あいうえお""",
25+
dirname("""C:\あいうえお\かきくけこ""".cstr)!!.toKString(),
26+
)
27+
}.let(::println)
28+
assertFails {
29+
assertEquals(
30+
"""C:\一二三四""",
31+
dirname("""C:\一二三四\五六七八""".cstr)!!.toKString(),
32+
)
33+
}.let(::println)
34+
}
35+
36+
@Test
37+
fun parent() {
38+
assertEquals(Path("""C:\foo"""), Path("""C:\foo\bar""").parent)
39+
assertEquals(Path("""C:\あいうえお"""), Path("""C:\あいうえお\かきくけこ""").parent)
40+
assertEquals(Path("""C:\一二三四"""), Path("""C:\一二三四\五六七八""").parent)
41+
assertEquals(null, Path("""C:\""").parent)
42+
}
1043

11-
class SmokeFileTestWindowsMinGW {
1244
@Test
1345
fun uncParent() {
14-
assertEquals(Path("\\\\server"), Path("\\\\server\\share").parent)
15-
assertEquals(Path("\\\\server\\share"), Path("\\\\server\\share\\dir").parent)
46+
assertEquals(Path("""\\server\share"""), Path("""\\server\share\dir""").parent)
47+
// This is a root UNC path, so parent is
48+
assertEquals(null, Path("""\\server\share""").parent)
49+
}
50+
51+
@Test
52+
fun basename(){
53+
assertEquals("あいうえお", Path("""C:\あいうえお""").name)
54+
assertEquals("", Path("""C:\""").name)
55+
}
56+
57+
58+
@Test
59+
fun isAbs() {
60+
assertEquals(true, Path("""C:\foo""").isAbsolute, """C:\foo""")
61+
assertEquals(false, Path("""foo\bar""").isAbsolute, """foo\bar""")
62+
assertEquals(true, Path("""\foo\bar""").isAbsolute, """\foo\bar""")
63+
assertEquals(true, Path("""C:\""").isAbsolute, """C:\""")
64+
assertEquals(true, Path("""\\server\share\dir""").isAbsolute, """\\server\share\dir""")
65+
}
66+
67+
@Test
68+
fun testFormatError() {
69+
val s = formatWin32ErrorMessage(ERROR_TOO_MANY_OPEN_FILES.toUInt())
70+
// it should be trimmed, drop the trailing rubbish
71+
assertEquals(s.trim(), s.trim())
72+
}
73+
74+
@Test
75+
fun testReadDir() {
76+
val expected = listOf("foo", "いろは歌", "天地玄黄")
77+
val actual = SystemFileSystem.list(Path("""./mingw/testdir""")).map { it.name }.sorted()
78+
assertEquals(expected, actual)
1679
}
1780
}

core/mingw/testdir/foo

Whitespace-only changes.

core/mingw/testdir/いろは歌

Whitespace-only changes.

core/mingw/testdir/天地玄黄

Whitespace-only changes.

0 commit comments

Comments
 (0)