Skip to content

Commit

Permalink
Support directory listing
Browse files Browse the repository at this point in the history
Closes #222
  • Loading branch information
fzhinkin committed Mar 18, 2024
1 parent bae3fe3 commit 7d19126
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,24 @@ kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
common {
group("native") {
group("nonApple") {
group("mingw")
group("unix") {
group("linux")
group("androidNative")
group("nonJvm") {
group("native") {
group("nonApple") {
group("mingw")
group("unix") {
group("linux")
group("androidNative")
}
}
}
}
group("nodeFilesystemShared") {
withJs()
withWasmJs()
}
group("wasm") {
withWasmJs()
withWasmWasi()
group("nodeFilesystemShared") {
withJs()
withWasmJs()
}
group("wasm") {
withWasmJs()
withWasmWasi()
}
}
}
}
Expand Down
25 changes: 23 additions & 2 deletions core/androidNative/src/files/FileSystemAndroid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

package kotlinx.io.files

import kotlinx.cinterop.CPointer
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.get
import kotlinx.cinterop.toKString
import platform.posix.__posix_basename
import platform.posix.dirname
import kotlinx.io.IOException
import platform.posix.*

@OptIn(ExperimentalForeignApi::class)
internal actual fun dirnameImpl(path: String): String {
Expand All @@ -24,3 +26,22 @@ internal actual fun basenameImpl(path: String): String {
}

internal actual fun isAbsoluteImpl(path: String): Boolean = path.startsWith('/')

@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<cnames.structs.DIR>) : AutoCloseable {
actual fun readdir(): String? {
val entry = platform.posix.readdir(dir) ?: return null
return entry[0].d_name.toKString()
}

override fun close() {
closedir(dir)
}
}

@OptIn(ExperimentalForeignApi::class)
internal actual fun opendir(path: String): OpaqueDirEntry {
val dirent = platform.posix.opendir(path)
if (dirent != null) return OpaqueDirEntry(dirent)
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
}
1 change: 1 addition & 0 deletions core/api/kotlinx-io-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ public abstract interface class kotlinx/io/files/FileSystem {
public abstract fun delete (Lkotlinx/io/files/Path;Z)V
public static synthetic fun delete$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)V
public abstract fun exists (Lkotlinx/io/files/Path;)Z
public abstract fun list (Lkotlinx/io/files/Path;)Ljava/util/List;
public abstract fun metadataOrNull (Lkotlinx/io/files/Path;)Lkotlinx/io/files/FileMetadata;
public abstract fun resolve (Lkotlinx/io/files/Path;)Lkotlinx/io/files/Path;
public abstract fun sink (Lkotlinx/io/files/Path;Z)Lkotlinx/io/RawSink;
Expand Down
25 changes: 21 additions & 4 deletions core/apple/src/files/FileSystemApple.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.NSTemporaryDirectory
import platform.posix.*
Expand Down Expand Up @@ -55,3 +52,23 @@ internal actual fun realpathImpl(path: String): String {
free(res)
}
}


@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<DIR>) : AutoCloseable {
actual fun readdir(): String? {
val entry = readdir(dir) ?: return null
return entry[0].d_name.toKString()
}

override fun close() {
closedir(dir)
}
}

@OptIn(ExperimentalForeignApi::class)
internal actual fun opendir(path: String): OpaqueDirEntry {
val dirent = platform.posix.opendir(path)
if (dirent != null) return OpaqueDirEntry(dirent)
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
}
14 changes: 14 additions & 0 deletions core/common/src/files/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,20 @@ public sealed interface FileSystem {
* @throws FileNotFoundException if there is no file or directory corresponding to the specified path.
*/
public fun resolve(path: Path): Path

/**
* Returns a list of paths corresponding to [directory]'s immediate children.
*
* If path [directory] was an absolute path, a returned list will also contain absolute paths.
* If it was a relative path, a returned list will contain relative paths.
*
* @param directory a directory to list.
* @return a list of [directory]'s immediate children.
* @throws FileNotFoundException if [directory] does not exist.
* @throws IOException if [directory] points to something other than directory.
* @throws IOException if there was an underlying error preventing listing [directory] children.
*/
public fun list(directory: Path): List<Path>
}

internal abstract class SystemFileSystemImpl : FileSystem
Expand Down
36 changes: 35 additions & 1 deletion core/common/test/files/SmokeFileTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class SmokeFileTest {
var lastException: Throwable? = null
files.forEach {
try {
SystemFileSystem.delete(it, false)
it.deleteRecursively()
} catch (t: Throwable) {
lastException = t
}
Expand All @@ -26,6 +26,14 @@ class SmokeFileTest {
}
}

private fun Path.deleteRecursively() {
val md = SystemFileSystem.metadataOrNull(this) ?: return
if (md.isDirectory) {
SystemFileSystem.list(this).forEach { it.deleteRecursively() }
}
SystemFileSystem.delete(this)
}

private fun createTempPath(): Path {
val f = Path(tempFileName())
files.add(f)
Expand Down Expand Up @@ -443,6 +451,32 @@ class SmokeFileTest {
source.close() // there should be no error
}

@Test
fun listDirectory() {
assertFailsWith<FileNotFoundException> { SystemFileSystem.list(createTempPath()) }

val tmpFile = createTempPath().also {
SystemFileSystem.sink(it).close()
}
assertFailsWith<IOException> { SystemFileSystem.list(tmpFile) }

val dir = createTempPath().also {
SystemFileSystem.createDirectories(it)
}
assertEquals(emptyList(), SystemFileSystem.list(dir))

val subdir = Path(dir, "subdir").also {
SystemFileSystem.createDirectories(it)
SystemFileSystem.sink(Path(it, "file")).close()
}
assertEquals(listOf(subdir), SystemFileSystem.list(dir))

val file = Path(dir, "file").also {
SystemFileSystem.sink(it).close()
}
assertEquals(listOf(file, subdir), SystemFileSystem.list(dir))
}

private fun constructAbsolutePath(vararg parts: String): String {
return SystemPathSeparator.toString() + parts.joinToString(SystemPathSeparator.toString())
}
Expand Down
11 changes: 11 additions & 0 deletions core/jvm/src/files/FileSystemJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
if (!path.file.exists()) throw FileNotFoundException(path.file.absolutePath)
return Path(path.file.canonicalFile)
}

override fun list(directory: Path): List<Path> {
val file = directory.file
if (!file.exists()) throw FileNotFoundException(file.absolutePath)
if (!file.isDirectory) throw IOException("Not a directory: ${file.absolutePath}")
return buildList {
file.list()?.forEach { childName ->
add(Path(directory, childName))
}
}
}
}

@JvmField
Expand Down
28 changes: 22 additions & 6 deletions core/linux/src/files/FileSystemLinux.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@

package kotlinx.io.files

import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cstr
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString
import platform.posix.__xpg_basename
import platform.posix.dirname
import kotlinx.cinterop.*
import kotlinx.io.IOException
import platform.posix.*

@OptIn(ExperimentalForeignApi::class)
internal actual fun dirnameImpl(path: String): String {
Expand All @@ -30,3 +27,22 @@ internal actual fun basenameImpl(path: String): String {
}

internal actual fun isAbsoluteImpl(path: String): Boolean = path.startsWith('/')

@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<DIR>) : AutoCloseable {
actual fun readdir(): String? {
val entry = readdir(dir) ?: return null
return entry[0].d_name.toKString()
}

override fun close() {
closedir(dir)
}
}

@OptIn(ExperimentalForeignApi::class)
internal actual fun opendir(path: String): OpaqueDirEntry {
val dirent = platform.posix.opendir(path)
if (dirent != null) return OpaqueDirEntry(dirent)
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
}
20 changes: 20 additions & 0 deletions core/mingw/src/files/FileSystemMingw.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,23 @@ internal actual fun realpathImpl(path: String): String {
return buffer.toKString()
}
}


@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
internal actual class OpaqueDirEntry constructor(private val dir: CPointer<DIR>) : AutoCloseable {
actual fun readdir(): String? {
val entry = readdir(dir) ?: return null
return entry[0].d_name.toKString()
}

override fun close() {
closedir(dir)
}
}

@OptIn(ExperimentalForeignApi::class)
internal actual fun opendir(path: String): OpaqueDirEntry {
val dirent = platform.posix.opendir(path)
if (dirent != null) return OpaqueDirEntry(dirent)
throw IOException("Can't open directory $path: ${strerror(errno)?.toKString() ?: "reason unknown"}")
}
25 changes: 24 additions & 1 deletion core/native/src/files/FileSystemNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import kotlinx.io.RawSource
import platform.posix.*
import kotlin.experimental.ExperimentalNativeApi

@OptIn(ExperimentalForeignApi::class)
@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() {
override fun exists(path: Path): Boolean {
return access(path.path, F_OK) == 0
Expand Down Expand Up @@ -102,6 +102,22 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
?: throw IOException("Failed to open $path with ${strerror(errno)?.toKString()}")
return FileSink(openFile)
}

override fun list(directory: Path): List<Path> {
val metadata = metadataOrNull(directory) ?: throw FileNotFoundException(directory.path)
if (!metadata.isDirectory) throw IOException("Not a directory: ${directory.path}")
return buildList {
opendir(directory.path).use {
var child = it.readdir()
while (child != null) {
if (child != "." && child != "..") {
add(Path(directory, child))
}
child = it.readdir()
}
}
}
}
}

internal expect fun atomicMoveImpl(source: Path, destination: Path)
Expand All @@ -119,3 +135,10 @@ internal const val PermissionAllowAll: UShort = 511u

@OptIn(ExperimentalNativeApi::class)
internal actual val isWindows: Boolean = Platform.osFamily == OsFamily.WINDOWS

@OptIn(ExperimentalStdlibApi::class)
internal expect class OpaqueDirEntry : AutoCloseable {
fun readdir(): String?
}

internal expect fun opendir(path: String): OpaqueDirEntry
17 changes: 17 additions & 0 deletions core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
if (!exists(path)) throw FileNotFoundException(path.path)
return Path(fs.realpathSync.native(path.path))
}

override fun list(directory: Path): List<Path> {
val metadata = metadataOrNull(directory) ?: throw FileNotFoundException(directory.path)
if (!metadata.isDirectory) throw IOException("Not a directory: ${directory.path}")
val dir = fs.opendirSync(directory.path) ?: throw IOException("Unable to read directory: ${directory.path}")
try {
return buildList {
var child = dir.readSync()
while (child != null) {
add(Path(directory, child.name))
child = dir.readSync()
}
}
} finally {
dir.closeSync()
}
}
}

public actual val SystemTemporaryDirectory: Path
Expand Down
21 changes: 21 additions & 0 deletions core/nodeFilesystemShared/src/node/fs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ internal external interface Fs {
*/
fun writeFileSync(fd: Int, buffer: Buffer)

/**
* See https://nodejs.org/api/fs.html#fsopendirsyncpath-options
*/
fun opendirSync(path: String): Dir?

val realpathSync: realpathSync

val constants: constants
Expand Down Expand Up @@ -86,4 +91,20 @@ internal external interface realpathSync {
fun native(path: String): String
}

/**
* See https://nodejs.org/api/fs.html#class-fsdir
*/
internal external interface Dir {
fun closeSync()

fun readSync(): Dirent?
}

/**
* See https://nodejs.org/api/fs.html#class-fsdirent
*/
internal external interface Dirent {
val name: String
}

internal expect val fs: Fs
11 changes: 11 additions & 0 deletions core/wasmWasi/src/-WasmUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,14 @@ internal fun Pointer.allocateString(value: String): Int {
*/
@UnsafeWasmMemoryApi
internal fun MemoryAllocator.allocateInt(): Pointer = allocate(Int.SIZE_BYTES)

/**
* Decodes zero-terminated string from a sequence of bytes that should not exceed [maxLength] bytes in length.
*/
@UnsafeWasmMemoryApi
internal fun Pointer.loadString(maxLength: Int): String {
val bytes = loadBytes(maxLength)
val firstZeroByte = bytes.indexOf(0)
val length = if (firstZeroByte == -1) maxLength else firstZeroByte
return bytes.decodeToString(0, length)
}
Loading

0 comments on commit 7d19126

Please sign in to comment.