Skip to content

Commit 8d87350

Browse files
authored
Support directory listing (#284)
Closes #222
1 parent 2d134d7 commit 8d87350

File tree

19 files changed

+311
-16
lines changed

19 files changed

+311
-16
lines changed

build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,19 @@ kotlin {
7373
applyDefaultHierarchyTemplate {
7474
common {
7575
group("native") {
76-
group("nonApple") {
76+
group("nativeNonApple") {
7777
group("mingw")
7878
group("unix") {
7979
group("linux")
8080
group("androidNative")
8181
}
8282
}
83+
84+
group("nativeNonAndroid") {
85+
group("apple")
86+
group("mingw")
87+
group("linux")
88+
}
8389
}
8490
group("nodeFilesystemShared") {
8591
withJs()

core/Module.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,8 @@ Core IO primitives.
8282
# Package kotlinx.io.files
8383

8484
Basic API for working with files.
85+
86+
#### Known issues
87+
88+
- [#312](https://github.com/Kotlin/kotlinx-io/issues/312) For `wasmWasi` target, directory listing ([kotlinx.io.files.FileSystem.list]) does not work with NodeJS runtime on Windows,
89+
as `fd_readdir` function is [not implemented there](https://github.com/nodejs/node/blob/6f4d6011ea1b448cf21f5d363c44e4a4c56ca34c/deps/uvwasi/src/uvwasi.c#L19).

core/androidNative/src/files/FileSystemAndroid.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
package kotlinx.io.files
77

8+
import kotlinx.cinterop.CPointer
89
import kotlinx.cinterop.ExperimentalForeignApi
10+
import kotlinx.cinterop.get
911
import kotlinx.cinterop.toKString
10-
import platform.posix.__posix_basename
11-
import platform.posix.dirname
12+
import kotlinx.io.IOException
13+
import platform.posix.*
1214

1315
@OptIn(ExperimentalForeignApi::class)
1416
internal actual fun dirnameImpl(path: String): String {
@@ -24,3 +26,22 @@ internal actual fun basenameImpl(path: String): String {
2426
}
2527

2628
internal actual fun isAbsoluteImpl(path: String): Boolean = path.startsWith('/')
29+
30+
@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
31+
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<cnames.structs.DIR>) : AutoCloseable {
32+
actual fun readdir(): String? {
33+
val entry = platform.posix.readdir(dir) ?: return null
34+
return entry[0].d_name.toKString()
35+
}
36+
37+
override fun close() {
38+
closedir(dir)
39+
}
40+
}
41+
42+
@OptIn(ExperimentalForeignApi::class)
43+
internal actual fun opendir(path: String): OpaqueDirEntry {
44+
val dirent = platform.posix.opendir(path)
45+
if (dirent != null) return OpaqueDirEntry(dirent)
46+
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
47+
}

core/api/kotlinx-io-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ public abstract interface class kotlinx/io/files/FileSystem {
215215
public abstract fun delete (Lkotlinx/io/files/Path;Z)V
216216
public static synthetic fun delete$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)V
217217
public abstract fun exists (Lkotlinx/io/files/Path;)Z
218+
public abstract fun list (Lkotlinx/io/files/Path;)Ljava/util/Collection;
218219
public abstract fun metadataOrNull (Lkotlinx/io/files/Path;)Lkotlinx/io/files/FileMetadata;
219220
public abstract fun resolve (Lkotlinx/io/files/Path;)Lkotlinx/io/files/Path;
220221
public abstract fun sink (Lkotlinx/io/files/Path;Z)Lkotlinx/io/RawSink;

core/api/kotlinx-io-core.klib.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ sealed interface kotlinx.io.files/FileSystem { // kotlinx.io.files/FileSystem|nu
164164
abstract fun createDirectories(kotlinx.io.files/Path, kotlin/Boolean =...) // kotlinx.io.files/FileSystem.createDirectories|createDirectories(kotlinx.io.files.Path;kotlin.Boolean){}[0]
165165
abstract fun delete(kotlinx.io.files/Path, kotlin/Boolean =...) // kotlinx.io.files/FileSystem.delete|delete(kotlinx.io.files.Path;kotlin.Boolean){}[0]
166166
abstract fun exists(kotlinx.io.files/Path): kotlin/Boolean // kotlinx.io.files/FileSystem.exists|exists(kotlinx.io.files.Path){}[0]
167+
abstract fun list(kotlinx.io.files/Path): kotlin.collections/Collection<kotlinx.io.files/Path> // kotlinx.io.files/FileSystem.list|list(kotlinx.io.files.Path){}[0]
167168
abstract fun metadataOrNull(kotlinx.io.files/Path): kotlinx.io.files/FileMetadata? // kotlinx.io.files/FileSystem.metadataOrNull|metadataOrNull(kotlinx.io.files.Path){}[0]
168169
abstract fun resolve(kotlinx.io.files/Path): kotlinx.io.files/Path // kotlinx.io.files/FileSystem.resolve|resolve(kotlinx.io.files.Path){}[0]
169170
abstract fun sink(kotlinx.io.files/Path, kotlin/Boolean =...): kotlinx.io/RawSink // kotlinx.io.files/FileSystem.sink|sink(kotlinx.io.files.Path;kotlin.Boolean){}[0]

core/apple/src/files/FileSystemApple.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66

77
package kotlinx.io.files
88

9-
import kotlinx.cinterop.ExperimentalForeignApi
10-
import kotlinx.cinterop.cstr
11-
import kotlinx.cinterop.memScoped
12-
import kotlinx.cinterop.toKString
9+
import kotlinx.cinterop.*
1310
import kotlinx.io.IOException
1411
import platform.Foundation.*
1512
import platform.posix.*

core/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
44
*/
55

6+
import org.gradle.internal.os.OperatingSystem
67
import org.jetbrains.dokka.gradle.DokkaTaskPartial
78

89
plugins {
@@ -30,6 +31,17 @@ kotlin {
3031
}
3132
}
3233
}
34+
wasmWasi {
35+
nodejs {
36+
testTask {
37+
// fd_readdir is unsupported on Windows:
38+
// https://github.com/nodejs/node/blob/6f4d6011ea1b448cf21f5d363c44e4a4c56ca34c/deps/uvwasi/src/uvwasi.c#L19
39+
if (OperatingSystem.current().isWindows) {
40+
filter.setExcludePatterns("*SmokeFileTest.listDirectory")
41+
}
42+
}
43+
}
44+
}
3345

3446
sourceSets {
3547
commonMain.dependencies {

core/common/src/files/FileSystem.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ public sealed interface FileSystem {
145145
* @throws FileNotFoundException if there is no file or directory corresponding to the specified path.
146146
*/
147147
public fun resolve(path: Path): Path
148+
149+
/**
150+
* Returns paths corresponding to [directory]'s immediate children.
151+
*
152+
* There are no guarantees on children paths order within a returned collection.
153+
*
154+
* If path [directory] was an absolute path, a returned collection will also contain absolute paths.
155+
* If it was a relative path, a returned collection will contain relative paths.
156+
*
157+
* *For `wasmWasi` target, function does not work with NodeJS runtime on Windows,
158+
* as `fd_readdir` function is [not implemented there](https://github.com/nodejs/node/blob/6f4d6011ea1b448cf21f5d363c44e4a4c56ca34c/deps/uvwasi/src/uvwasi.c#L19).*
159+
*
160+
* @param directory a directory to list.
161+
* @return a collection of [directory]'s immediate children.
162+
* @throws FileNotFoundException if [directory] does not exist.
163+
* @throws IOException if [directory] points to something other than directory.
164+
* @throws IOException if there was an underlying error preventing listing [directory] children.
165+
*/
166+
public fun list(directory: Path): Collection<Path>
148167
}
149168

150169
internal abstract class SystemFileSystemImpl : FileSystem

core/common/test/files/SmokeFileTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,36 @@ class SmokeFileTest {
443443
source.close() // there should be no error
444444
}
445445

446+
@Test
447+
fun listDirectory() {
448+
assertFailsWith<FileNotFoundException> { SystemFileSystem.list(createTempPath()) }
449+
450+
val tmpFile = createTempPath().also {
451+
SystemFileSystem.sink(it).close()
452+
}
453+
assertFailsWith<IOException> { SystemFileSystem.list(tmpFile) }
454+
455+
val dir = createTempPath().also {
456+
SystemFileSystem.createDirectories(it)
457+
}
458+
assertEquals(emptyList(), SystemFileSystem.list(dir))
459+
460+
val subdir = Path(dir, "subdir").also {
461+
SystemFileSystem.createDirectories(it)
462+
SystemFileSystem.sink(Path(it, "file")).close()
463+
}
464+
assertEquals(listOf(subdir), SystemFileSystem.list(dir))
465+
466+
val file = Path(dir, "file").also {
467+
SystemFileSystem.sink(it).close()
468+
}
469+
assertEquals(setOf(file, subdir), SystemFileSystem.list(dir).toSet())
470+
471+
SystemFileSystem.delete(file)
472+
SystemFileSystem.delete(Path(subdir, "file"))
473+
SystemFileSystem.delete(subdir)
474+
}
475+
446476
private fun constructAbsolutePath(vararg parts: String): String {
447477
return SystemPathSeparator.toString() + parts.joinToString(SystemPathSeparator.toString())
448478
}

core/jvm/src/files/FileSystemJvm.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
9696
if (!path.file.exists()) throw FileNotFoundException(path.file.absolutePath)
9797
return Path(path.file.canonicalFile)
9898
}
99+
100+
override fun list(directory: Path): Collection<Path> {
101+
val file = directory.file
102+
if (!file.exists()) throw FileNotFoundException(file.absolutePath)
103+
if (!file.isDirectory) throw IOException("Not a directory: ${file.absolutePath}")
104+
return buildList {
105+
file.list()?.forEach { childName ->
106+
add(Path(directory, childName))
107+
}
108+
}
109+
}
99110
}
100111

101112
@JvmField

0 commit comments

Comments
 (0)