Skip to content

Commit f6179c1

Browse files
committed
Support directory listing
Closes #222
1 parent 19f7291 commit f6179c1

File tree

14 files changed

+301
-22
lines changed

14 files changed

+301
-22
lines changed

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
@@ -213,6 +213,7 @@ public abstract interface class kotlinx/io/files/FileSystem {
213213
public abstract fun delete (Lkotlinx/io/files/Path;Z)V
214214
public static synthetic fun delete$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)V
215215
public abstract fun exists (Lkotlinx/io/files/Path;)Z
216+
public abstract fun list (Lkotlinx/io/files/Path;)Ljava/util/List;
216217
public abstract fun metadataOrNull (Lkotlinx/io/files/Path;)Lkotlinx/io/files/FileMetadata;
217218
public abstract fun resolve (Lkotlinx/io/files/Path;)Lkotlinx/io/files/Path;
218219
public abstract fun sink (Lkotlinx/io/files/Path;Z)Lkotlinx/io/RawSink;

core/apple/src/files/FileSystemApple.kt

Lines changed: 21 additions & 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.*
@@ -56,6 +53,26 @@ internal actual fun realpathImpl(path: String): String {
5653
}
5754
}
5855

56+
57+
@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
58+
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<DIR>) : AutoCloseable {
59+
actual fun readdir(): String? {
60+
val entry = readdir(dir) ?: return null
61+
return entry[0].d_name.toKString()
62+
}
63+
64+
override fun close() {
65+
closedir(dir)
66+
}
67+
}
68+
69+
@OptIn(ExperimentalForeignApi::class)
70+
internal actual fun opendir(path: String): OpaqueDirEntry {
71+
val dirent = platform.posix.opendir(path)
72+
if (dirent != null) return OpaqueDirEntry(dirent)
73+
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
74+
}
75+
5976
internal actual fun metadataOrNullImpl(path: Path): FileMetadata? {
6077
val attributes = NSFileManager.defaultManager().fileAttributesAtPath(path.path, traverseLink = true) ?: return null
6178
val fileType = attributes[NSFileType] as String

core/common/src/files/FileSystem.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ 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 a list of paths corresponding to [directory]'s immediate children.
151+
*
152+
* If path [directory] was an absolute path, a returned list will also contain absolute paths.
153+
* If it was a relative path, a returned list will contain relative paths.
154+
*
155+
* @param directory a directory to list.
156+
* @return a list of [directory]'s immediate children.
157+
* @throws FileNotFoundException if [directory] does not exist.
158+
* @throws IOException if [directory] points to something other than directory.
159+
* @throws IOException if there was an underlying error preventing listing [directory] children.
160+
*/
161+
public fun list(directory: Path): List<Path>
148162
}
149163

150164
internal abstract class SystemFileSystemImpl : FileSystem

core/common/test/files/SmokeFileTest.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class SmokeFileTest {
1616
var lastException: Throwable? = null
1717
files.forEach {
1818
try {
19-
SystemFileSystem.delete(it, false)
19+
it.deleteRecursively()
2020
} catch (t: Throwable) {
2121
lastException = t
2222
}
@@ -26,6 +26,14 @@ class SmokeFileTest {
2626
}
2727
}
2828

29+
private fun Path.deleteRecursively() {
30+
val md = SystemFileSystem.metadataOrNull(this) ?: return
31+
if (md.isDirectory) {
32+
SystemFileSystem.list(this).forEach { it.deleteRecursively() }
33+
}
34+
SystemFileSystem.delete(this)
35+
}
36+
2937
private fun createTempPath(): Path {
3038
val f = Path(tempFileName())
3139
files.add(f)
@@ -443,6 +451,32 @@ class SmokeFileTest {
443451
source.close() // there should be no error
444452
}
445453

454+
@Test
455+
fun listDirectory() {
456+
assertFailsWith<FileNotFoundException> { SystemFileSystem.list(createTempPath()) }
457+
458+
val tmpFile = createTempPath().also {
459+
SystemFileSystem.sink(it).close()
460+
}
461+
assertFailsWith<IOException> { SystemFileSystem.list(tmpFile) }
462+
463+
val dir = createTempPath().also {
464+
SystemFileSystem.createDirectories(it)
465+
}
466+
assertEquals(emptyList(), SystemFileSystem.list(dir))
467+
468+
val subdir = Path(dir, "subdir").also {
469+
SystemFileSystem.createDirectories(it)
470+
SystemFileSystem.sink(Path(it, "file")).close()
471+
}
472+
assertEquals(listOf(subdir), SystemFileSystem.list(dir))
473+
474+
val file = Path(dir, "file").also {
475+
SystemFileSystem.sink(it).close()
476+
}
477+
assertEquals(listOf(file, subdir), SystemFileSystem.list(dir))
478+
}
479+
446480
private fun constructAbsolutePath(vararg parts: String): String {
447481
return SystemPathSeparator.toString() + parts.joinToString(SystemPathSeparator.toString())
448482
}

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): List<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

core/linux/src/files/FileSystemLinux.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55

66
package kotlinx.io.files
77

8-
import kotlinx.cinterop.ExperimentalForeignApi
9-
import kotlinx.cinterop.cstr
10-
import kotlinx.cinterop.memScoped
11-
import kotlinx.cinterop.toKString
12-
import platform.posix.__xpg_basename
13-
import platform.posix.dirname
8+
import kotlinx.cinterop.*
9+
import kotlinx.io.IOException
10+
import platform.posix.*
1411

1512
@OptIn(ExperimentalForeignApi::class)
1613
internal actual fun dirnameImpl(path: String): String {
@@ -30,3 +27,22 @@ internal actual fun basenameImpl(path: String): String {
3027
}
3128

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

core/mingw/src/files/FileSystemMingw.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,23 @@ internal actual fun realpathImpl(path: String): String {
6060
return buffer.toKString()
6161
}
6262
}
63+
64+
65+
@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
66+
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<DIR>) : AutoCloseable {
67+
actual fun readdir(): String? {
68+
val entry = readdir(dir) ?: return null
69+
return entry[0].d_name.toKString()
70+
}
71+
72+
override fun close() {
73+
closedir(dir)
74+
}
75+
}
76+
77+
@OptIn(ExperimentalForeignApi::class)
78+
internal actual fun opendir(path: String): OpaqueDirEntry {
79+
val dirent = platform.posix.opendir(path)
80+
if (dirent != null) return OpaqueDirEntry(dirent)
81+
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
82+
}

core/native/src/files/FileSystemNative.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlinx.io.RawSource
1212
import platform.posix.*
1313
import kotlin.experimental.ExperimentalNativeApi
1414

15-
@OptIn(ExperimentalForeignApi::class)
15+
@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
1616
public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() {
1717
override fun exists(path: Path): Boolean {
1818
return access(path.path, F_OK) == 0
@@ -86,6 +86,22 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
8686
?: throw IOException("Failed to open $path with ${strerror(errno)?.toKString()}")
8787
return FileSink(openFile)
8888
}
89+
90+
override fun list(directory: Path): List<Path> {
91+
val metadata = metadataOrNull(directory) ?: throw FileNotFoundException(directory.path)
92+
if (!metadata.isDirectory) throw IOException("Not a directory: ${directory.path}")
93+
return buildList {
94+
opendir(directory.path).use {
95+
var child = it.readdir()
96+
while (child != null) {
97+
if (child != "." && child != "..") {
98+
add(Path(directory, child))
99+
}
100+
child = it.readdir()
101+
}
102+
}
103+
}
104+
}
89105
}
90106

91107
internal expect fun metadataOrNullImpl(path: Path): FileMetadata?
@@ -105,3 +121,10 @@ internal const val PermissionAllowAll: UShort = 511u
105121

106122
@OptIn(ExperimentalNativeApi::class)
107123
internal actual val isWindows: Boolean = Platform.osFamily == OsFamily.WINDOWS
124+
125+
@OptIn(ExperimentalStdlibApi::class)
126+
internal expect class OpaqueDirEntry : AutoCloseable {
127+
fun readdir(): String?
128+
}
129+
130+
internal expect fun opendir(path: String): OpaqueDirEntry

core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,23 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
100100
if (!exists(path)) throw FileNotFoundException(path.path)
101101
return Path(fs.realpathSync.native(path.path))
102102
}
103+
104+
override fun list(directory: Path): List<Path> {
105+
val metadata = metadataOrNull(directory) ?: throw FileNotFoundException(directory.path)
106+
if (!metadata.isDirectory) throw IOException("Not a directory: ${directory.path}")
107+
val dir = fs.opendirSync(directory.path) ?: throw IOException("Unable to read directory: ${directory.path}")
108+
try {
109+
return buildList {
110+
var child = dir.readSync()
111+
while (child != null) {
112+
add(Path(directory, child.name))
113+
child = dir.readSync()
114+
}
115+
}
116+
} finally {
117+
dir.closeSync()
118+
}
119+
}
103120
}
104121

105122
public actual val SystemTemporaryDirectory: Path

core/nodeFilesystemShared/src/node/fs.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ internal external interface Fs {
5656
*/
5757
fun writeFileSync(fd: Int, buffer: Buffer)
5858

59+
/**
60+
* See https://nodejs.org/api/fs.html#fsopendirsyncpath-options
61+
*/
62+
fun opendirSync(path: String): Dir?
63+
5964
val realpathSync: realpathSync
6065

6166
val constants: constants
@@ -86,4 +91,20 @@ internal external interface realpathSync {
8691
fun native(path: String): String
8792
}
8893

94+
/**
95+
* See https://nodejs.org/api/fs.html#class-fsdir
96+
*/
97+
internal external interface Dir {
98+
fun closeSync()
99+
100+
fun readSync(): Dirent?
101+
}
102+
103+
/**
104+
* See https://nodejs.org/api/fs.html#class-fsdirent
105+
*/
106+
internal external interface Dirent {
107+
val name: String
108+
}
109+
89110
internal expect val fs: Fs

core/wasmWasi/src/-WasmUtils.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,14 @@ internal fun Pointer.allocateString(value: String): Int {
109109
*/
110110
@UnsafeWasmMemoryApi
111111
internal fun MemoryAllocator.allocateInt(): Pointer = allocate(Int.SIZE_BYTES)
112+
113+
/**
114+
* Decodes zero-terminated string from a sequence of bytes that should not exceed [maxLength] bytes in length.
115+
*/
116+
@UnsafeWasmMemoryApi
117+
internal fun Pointer.loadString(maxLength: Int): String {
118+
val bytes = loadBytes(maxLength)
119+
val firstZeroByte = bytes.indexOf(0)
120+
val length = if (firstZeroByte == -1) maxLength else firstZeroByte
121+
return bytes.decodeToString(0, length)
122+
}

0 commit comments

Comments
 (0)