diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba836b7bae..3185f9a324 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,10 +59,18 @@ jobs: - uses: gradle/actions/setup-gradle@v6 + - name: Build Android native test binary + run: ./gradlew :okio:androidNativeX64TestBinaries + - uses: reactivecircus/android-emulator-runner@v2 with: api-level: 24 - script: ./gradlew :okio-assetfilesystem:connectedCheck + arch: x86_64 + script: | + adb push okio/build/bin/androidNativeX64/debugTest/test.kexe /data/local/tmp + adb push okio-testing-support /data/local/tmp/okio/okio-testing-support + adb shell "OKIO_ROOT=/data/local/tmp/okio /data/local/tmp/test.kexe" + ./gradlew :okio-assetfilesystem:connectedCheck loom: runs-on: ubuntu-latest diff --git a/build-support/src/main/kotlin/platforms.kt b/build-support/src/main/kotlin/platforms.kt index ee233310e2..4783f14048 100644 --- a/build-support/src/main/kotlin/platforms.kt +++ b/build-support/src/main/kotlin/platforms.kt @@ -34,6 +34,10 @@ fun KotlinMultiplatformExtension.configureOrCreateOkioPlatforms() { } fun KotlinMultiplatformExtension.configureOrCreateNativePlatforms() { + androidNativeArm64() + androidNativeArm32() + androidNativeX64() + androidNativeX86() iosX64() iosArm64() iosSimulatorArm64() @@ -53,6 +57,13 @@ fun KotlinMultiplatformExtension.configureOrCreateNativePlatforms() { mingwX64() } +val androidNativeTargets = listOf( + "androidNativeArm64", + "androidNativeArm32", + "androidNativeX64", + "androidNativeX86", +) + val appleTargets = listOf( "iosArm64", "iosX64", @@ -78,7 +89,7 @@ val linuxTargets = listOf( "linuxArm64", ) -val nativeTargets = appleTargets + linuxTargets + mingwTargets +val nativeTargets = androidNativeTargets + appleTargets + linuxTargets + mingwTargets val wasmTargets = listOf( "wasmJs", diff --git a/okio-fakefilesystem/api/okio-fakefilesystem.klib.api b/okio-fakefilesystem/api/okio-fakefilesystem.klib.api index 2e42041f7d..475ac2db79 100644 --- a/okio-fakefilesystem/api/okio-fakefilesystem.klib.api +++ b/okio-fakefilesystem/api/okio-fakefilesystem.klib.api @@ -1,5 +1,5 @@ // Klib ABI Dump -// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt b/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt index f669b3eb2a..9f51de81a6 100644 --- a/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt +++ b/okio-testing-support/src/commonMain/kotlin/okio/AbstractFileSystemTest.kt @@ -52,6 +52,8 @@ abstract class AbstractFileSystemTest( private val isNodeJsFileSystem = fileSystem::class.simpleName?.startsWith("NodeJs") ?: false private val isWasiFileSystem = fileSystem::class.simpleName?.startsWith("Wasi") ?: false private val isWrappingJimFileSystem = this::class.simpleName?.contains("JimFileSystem") ?: false + private val isAndroidNativeFileSystem = fileSystem::class.simpleName?.startsWith("Posix") ?: false && + getEnv("ANDROID_DATA") != null @Test fun doesNotExistsWithInvalidPathDoesNotThrow() { @@ -283,6 +285,7 @@ abstract class AbstractFileSystemTest( @Test fun listOnRelativePathWhichIsNotDotReturnsRelativePaths() { if (isNodeJsFileSystem) return + if (isAndroidNativeFileSystem) return val apiDir = "api".toPath() val expectedFiles = listOf( @@ -336,6 +339,7 @@ abstract class AbstractFileSystemTest( @Test fun listOrNullOnRelativePathWhichIsNotDotReturnsRelativePaths() { if (isNodeJsFileSystem) return + if (isAndroidNativeFileSystem) return val apiDir = "api".toPath() val expectedFiles = listOf( diff --git a/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt b/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt index 24cf7b82c7..1f8a90e029 100644 --- a/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt +++ b/okio-testing-support/src/commonMain/kotlin/okio/TestingCommon.kt @@ -46,7 +46,7 @@ expect fun isBrowser(): Boolean * The file system that GitHub actions gives us doesn't do anything when we `touch` a file. */ val fileSystemHasGoodMetadata: Boolean - get() = getEnv("GITHUB_WORKSPACE") == null + get() = getEnv("GITHUB_WORKSPACE") == null && getEnv("ANDROID_DATA") == null val FileMetadata.createdAt: Instant? get() { diff --git a/okio/api/okio.klib.api b/okio/api/okio.klib.api index 5effa9a883..facb5b2ff4 100644 --- a/okio/api/okio.klib.api +++ b/okio/api/okio.klib.api @@ -1,6 +1,6 @@ // Klib ABI Dump -// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] -// Alias: native => [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Alias: native => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Alias: apple => [iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 diff --git a/okio/build.gradle.kts b/okio/build.gradle.kts index b2c5a29f57..158a212a25 100644 --- a/okio/build.gradle.kts +++ b/okio/build.gradle.kts @@ -40,7 +40,12 @@ plugins { * | | |-- watchosArm64 * | '-- linux * | |-- linuxX64 - * | '-- linuxArm64 + * | |-- linuxArm64 + * | '-- androidNative + * | |-- androidNativeArm64 + * | |-- androidNativeArm32 + * | |-- androidNativeX64 + * | |-- androidNativeX86 * '-- wasm * '-- wasmJs * '-- wasmWasi @@ -171,6 +176,8 @@ kotlin { children = linuxTargets, ).also { linuxMain -> linuxMain.dependsOn(nonAppleMain) + createSourceSet("linuxNonAndroidMain", parent = linuxMain, children = linuxTargets) + createSourceSet("androidNativeMain", parent = linuxMain, children = androidNativeTargets) } createSourceSet( name = "appleMain", @@ -183,10 +190,11 @@ kotlin { children = appleTargets - "macosX64", ) } + createSourceSet("unixNonAndroidMain", parent = unixMain, children = appleTargets + linuxTargets) } } - createSourceSet("nativeTest", parent = commonTest, children = mingwTargets + linuxTargets) + createSourceSet("nativeTest", parent = commonTest, children = androidNativeTargets + mingwTargets + linuxTargets) .also { nativeTest -> nativeTest.dependsOn(nonJvmTest) nativeTest.dependsOn(nonWasmTest) @@ -210,7 +218,7 @@ kotlin { } targets.withType { - if (konanTarget.family == Family.LINUX) { + if (konanTarget.family == Family.LINUX || konanTarget.family == Family.ANDROID) { compilations["main"].cinterops.create("linux") { packageName("okio.internal.linux") headers( diff --git a/okio/src/androidNativeMain/kotlin/okio/AndroidPosixVariant.kt b/okio/src/androidNativeMain/kotlin/okio/AndroidPosixVariant.kt new file mode 100644 index 0000000000..7b5c21cd70 --- /dev/null +++ b/okio/src/androidNativeMain/kotlin/okio/AndroidPosixVariant.kt @@ -0,0 +1,11 @@ +package okio + +import kotlin.concurrent.Volatile +import platform.posix.android_get_device_api_level + +/** + * Checked eagerly because on older Android versions (API < 30) + * seccomp blocks the syscall with SIGSYS, crashing the app instead of returning ENOSYS. + **/ +@Volatile +internal actual var isStatXSupported = android_get_device_api_level() >= 30 diff --git a/okio/src/androidNativeMain/kotlin/okio/internal/PosixDirectory.kt b/okio/src/androidNativeMain/kotlin/okio/internal/PosixDirectory.kt new file mode 100644 index 0000000000..2f6e2ff1b4 --- /dev/null +++ b/okio/src/androidNativeMain/kotlin/okio/internal/PosixDirectory.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio.internal + +import cnames.structs.DIR +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.reinterpret +import okio.Path +import platform.posix.closedir +import platform.posix.opendir +import platform.posix.readdir + +private class AndroidPosixDirectory(private val dir: CPointer) : PosixDirectory { + override fun nextEntry() = readdir(dir.reinterpret()) + override fun close() { + closedir(dir.reinterpret()) // Ignore errno from closedir. + } +} + +internal actual fun openPosixDirectory(path: Path): PosixDirectory? { + return opendir(path.toString())?.let(::AndroidPosixDirectory) +} diff --git a/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt b/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt index 4fdd049d59..25205816f2 100644 --- a/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt +++ b/okio/src/linuxMain/kotlin/okio/LinuxPosixVariant.kt @@ -17,6 +17,7 @@ package okio import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr import okio.internal.linux.AT_FDCWD @@ -36,51 +37,61 @@ import platform.posix.lstat import platform.posix.stat import platform.posix.syscall +internal expect var isStatXSupported: Boolean + /** * Prefer `statx()` if it's available. Fall back to `stat()` which doesn't have a field for * `createdAt`. */ +@OptIn(UnsafeNumber::class) internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? { memScoped { - val statx = alloc() - val result = syscall( - __NR_statx.toLong(), - AT_FDCWD, - path.toString(), - AT_SYMLINK_NOFOLLOW, - STATX_BASIC_STATS or STATX_BTIME, - statx.ptr, - ) - if (result == 0L) { - return FileMetadata( - isRegularFile = statx.stx_mode.toInt() and S_IFMT == S_IFREG, - isDirectory = statx.stx_mode.toInt() and S_IFMT == S_IFDIR, - symlinkTarget = symlinkTarget(statx.stx_mode.toInt(), path), - size = statx.stx_size.toLong(), - createdAtMillis = when { - statx.stx_mask and STATX_BTIME != 0U -> statx.stx_btime.epochMillis - else -> statx.stx_mtime.epochMillis - }, - lastModifiedAtMillis = statx.stx_mtime.epochMillis, - lastAccessedAtMillis = statx.stx_atime.epochMillis, - ) - } - - // Recover if statx() isn't available. It first appeared in Linux in 4.11 (2017-04-30) and - // Android in API 30 (2020-09-08). - if (errno == ENOSYS) { - val stat = alloc() - if (lstat(path.toString(), stat.ptr) == 0) { + if (isStatXSupported) { + val statx = alloc() + val result = syscall( + __NR_statx.convert(), + AT_FDCWD, + path.toString(), + AT_SYMLINK_NOFOLLOW, + STATX_BASIC_STATS or STATX_BTIME, + statx.ptr, + ).convert() + if (result == 0) { return FileMetadata( - isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG, - isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR, - symlinkTarget = symlinkTarget(stat.st_mode.toInt(), path), - size = stat.st_size, - createdAtMillis = stat.st_mtim.epochMillis, - lastModifiedAtMillis = stat.st_mtim.epochMillis, - lastAccessedAtMillis = stat.st_atim.epochMillis, + isRegularFile = statx.stx_mode.toInt() and S_IFMT == S_IFREG, + isDirectory = statx.stx_mode.toInt() and S_IFMT == S_IFDIR, + symlinkTarget = symlinkTarget(statx.stx_mode.toInt(), path), + size = statx.stx_size.toLong(), + createdAtMillis = when { + statx.stx_mask and STATX_BTIME != 0U -> statx.stx_btime.epochMillis + else -> statx.stx_mtime.epochMillis + }, + lastModifiedAtMillis = statx.stx_mtime.epochMillis, + lastAccessedAtMillis = statx.stx_atime.epochMillis, ) } + + // Recover if statx() isn't available. It first appeared in Linux in 4.11 (2017-04-30) and + // Android in API 30 (2020-09-08). + if (errno == ENOSYS) { + isStatXSupported = false + } else { + if (errno == ENOENT) return null + throw errnoToIOException(errno) + } + } + + val stat = alloc() + if (lstat(path.toString(), stat.ptr) == 0) { + return FileMetadata( + isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG, + isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR, + symlinkTarget = symlinkTarget(stat.st_mode.toInt(), path), + size = stat.st_size, + createdAtMillis = stat.st_mtim.epochMillis, + lastModifiedAtMillis = stat.st_mtim.epochMillis, + lastAccessedAtMillis = stat.st_atim.epochMillis, + ) } if (errno == ENOENT) return null diff --git a/okio/src/linuxNonAndroidMain/kotlin/okio/LinuxPosixVariant.kt b/okio/src/linuxNonAndroidMain/kotlin/okio/LinuxPosixVariant.kt new file mode 100644 index 0000000000..65184733ed --- /dev/null +++ b/okio/src/linuxNonAndroidMain/kotlin/okio/LinuxPosixVariant.kt @@ -0,0 +1,6 @@ +package okio + +import kotlin.concurrent.Volatile + +@Volatile +internal actual var isStatXSupported = true diff --git a/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt index 653ad9004e..3178e6fa03 100644 --- a/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt +++ b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt @@ -15,9 +15,7 @@ */ package okio -import kotlinx.cinterop.get import okio.Path.Companion.toPath -import okio.internal.toPath import platform.posix.EEXIST import platform.posix.errno diff --git a/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt b/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt index 62e7849f7e..7f57bdd5f1 100644 --- a/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt +++ b/okio/src/unixMain/kotlin/okio/UnixFileHandle.kt @@ -16,8 +16,10 @@ package okio import kotlinx.cinterop.CPointer +import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.addressOf import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr import kotlinx.cinterop.usePinned @@ -84,8 +86,9 @@ internal class UnixFileHandle( } } + @OptIn(UnsafeNumber::class) override fun protectedResize(size: Long) { - if (ftruncate(fileno(file), size) == -1) { + if (ftruncate(fileno(file), size.convert()) == -1) { throw errnoToIOException(errno) } } diff --git a/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt b/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt index aa643c82c9..eaee991895 100644 --- a/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt +++ b/okio/src/unixMain/kotlin/okio/UnixPosixVariant.kt @@ -25,8 +25,8 @@ import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.toKString import okio.Path.Companion.toPath +import okio.internal.openPosixDirectory import okio.internal.toPath -import platform.posix.DEFFILEMODE import platform.posix.ENOENT import platform.posix.FILE import platform.posix.O_CREAT @@ -35,7 +35,6 @@ import platform.posix.O_RDWR import platform.posix.PATH_MAX import platform.posix.S_IFLNK import platform.posix.S_IFMT -import platform.posix.closedir import platform.posix.dirent import platform.posix.errno import platform.posix.fdopen @@ -45,10 +44,8 @@ import platform.posix.free import platform.posix.getenv import platform.posix.mkdir import platform.posix.open -import platform.posix.opendir import platform.posix.pread import platform.posix.pwrite -import platform.posix.readdir import platform.posix.readlink import platform.posix.realpath import platform.posix.remove @@ -82,16 +79,16 @@ internal actual fun PosixFileSystem.variantDelete(path: Path, mustExist: Boolean } internal actual fun PosixFileSystem.variantList(dir: Path, throwOnFailure: Boolean): List? { - val opendir = opendir(dir.toString()) + val opendir = openPosixDirectory(dir) ?: if (throwOnFailure) throw errnoToIOException(errno) else return null - try { + opendir.use { val result = mutableListOf() val buffer = Buffer() set_posix_errno(0) // If readdir() returns null it's either the end or an error. while (true) { - val dirent: CPointer = readdir(opendir) ?: break + val dirent: CPointer = opendir.nextEntry() ?: break val childPath = buffer.writeNullTerminated( bytes = dirent[0].d_name, ).toPath(normalize = true) @@ -113,8 +110,6 @@ internal actual fun PosixFileSystem.variantList(dir: Path, throwOnFailure: Boole result.sort() return result - } finally { - closedir(opendir) // Ignore errno from closedir. } } @@ -172,6 +167,8 @@ internal actual fun PosixFileSystem.variantOpenReadOnly(file: Path): FileHandle return UnixFileHandle(false, openFile) } +internal const val DEFFILEMODE = 0b110110110 /* octal 666 */ + internal actual fun PosixFileSystem.variantOpenReadWrite( file: Path, mustCreate: Boolean, @@ -218,7 +215,7 @@ internal fun variantPread( target: CValuesRef<*>, byteCount: Int, offset: Long, -): Int = pread(fileno(file), target, byteCount.convert(), offset).convert() +): Int = pread(fileno(file), target, byteCount.convert(), offset.convert()).convert() @OptIn(UnsafeNumber::class) internal fun variantPwrite( @@ -226,7 +223,7 @@ internal fun variantPwrite( source: CValuesRef<*>, byteCount: Int, offset: Long, -): Int = pwrite(fileno(file), source, byteCount.convert(), offset).convert() +): Int = pwrite(fileno(file), source, byteCount.convert(), offset.convert()).convert() @OptIn(UnsafeNumber::class) internal val timespec.epochMillis: Long diff --git a/okio/src/unixMain/kotlin/okio/internal/PosixDirectory.kt b/okio/src/unixMain/kotlin/okio/internal/PosixDirectory.kt new file mode 100644 index 0000000000..967bece558 --- /dev/null +++ b/okio/src/unixMain/kotlin/okio/internal/PosixDirectory.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio.internal + +import kotlinx.cinterop.CPointer +import okio.Closeable +import okio.Path +import platform.posix.dirent + +/** + * `platform.posix.DIR` is not available on Android Native, so the standard + * POSIX directory APIs (`opendir`, `readdir`, `closedir`) cannot be used + * directly. + * + * [PosixDirectory] provides platform-specific implementation + * for `DIR`-related functionality. + */ +internal interface PosixDirectory : Closeable { + fun nextEntry(): CPointer? + override fun close() +} + +internal expect fun openPosixDirectory(path: Path): PosixDirectory? diff --git a/okio/src/unixNonAndroidMain/kotlin/okio/internal/PosixDirectory.kt b/okio/src/unixNonAndroidMain/kotlin/okio/internal/PosixDirectory.kt new file mode 100644 index 0000000000..dc1f1f49f5 --- /dev/null +++ b/okio/src/unixNonAndroidMain/kotlin/okio/internal/PosixDirectory.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio.internal + +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.reinterpret +import okio.Path +import platform.posix.DIR +import platform.posix.closedir +import platform.posix.opendir +import platform.posix.readdir + +private class UnixPosixDirectory(private val dir: CPointer) : PosixDirectory { + override fun nextEntry() = readdir(dir.reinterpret()) + override fun close() { + closedir(dir.reinterpret()) // Ignore errno from closedir. + } +} + +internal actual fun openPosixDirectory(path: Path): PosixDirectory? { + return opendir(path.toString())?.let(::UnixPosixDirectory) +}