From e954e23b887020f03af4e2e27d2003e675627562 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sun, 30 Jul 2023 12:36:09 -0400 Subject: [PATCH] Automatically get preopens via the API See https://github.com/WebAssembly/WASI/issues/323 --- .../wasmMain/kotlin/okio/WasiFileSystem.kt | 81 ++++++++++++------- .../kotlin/okio/internal/preview1/Preview1.kt | 23 ++++++ .../kotlin/okio/WasiFileSystemPreopensTest.kt | 11 +-- .../kotlin/okio/WasiFileSystemTest.kt | 2 +- .../src/wasmTest/kotlin/okio/WasiTest.kt | 2 +- 5 files changed, 76 insertions(+), 43 deletions(-) diff --git a/okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt b/okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt index 829e7406b7..1ae8a2b532 100644 --- a/okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt +++ b/okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt @@ -23,6 +23,8 @@ import okio.internal.fdClose import okio.internal.preview1.Errno import okio.internal.preview1.dirnamelen import okio.internal.preview1.fd +import okio.internal.preview1.fd_prestat_dir_name +import okio.internal.preview1.fd_prestat_get import okio.internal.preview1.fd_readdir import okio.internal.preview1.fdflags import okio.internal.preview1.fdflags_append @@ -59,18 +61,43 @@ import okio.internal.write * * [WASI]: https://wasi.dev/ */ -class WasiFileSystem( - private val relativePathPreopen: Int = DEFAULT_FIRST_PREOPEN, - pathToPreopen: Map = mapOf("/".toPath() to DEFAULT_FIRST_PREOPEN), -) : FileSystem() { - private val pathSegmentsToPreopen = pathToPreopen.mapKeys { (key, _) -> key.segmentsBytes } - - init { - require(pathSegmentsToPreopen.isNotEmpty()) { - "pathToPreopen must be non-empty" +object WasiFileSystem : FileSystem() { + private val pathToPreopen: Map = run { + // File descriptor of the first preopen in the `WASI` instance's configured `preopens` property. + // This is 3 by default, assuming `stdin` is 0, `stdout` is 1, and `stderr` is 2. Other preopens + // are assigned sequentially starting at this value. + val firstPreopen = 3 + + withScopedMemoryAllocator { allocator -> + val map = mutableMapOf() + + val bufSize = 2048 + val bufPointer = allocator.allocate(bufSize) + + for (fd in firstPreopen..Int.MAX_VALUE) { + val getReturnPointer = allocator.allocate(12) + + val getErrno = fd_prestat_get(fd, getReturnPointer.address.toInt()) + if (getErrno == Errno.badf.ordinal) break // No more preopens. + if (getErrno != 0) throw ErrnoException(getErrno.toShort()) + + val size = (getReturnPointer + 4).loadInt() + require(size + 1 < bufSize) { "unexpected preopen size: $size" } + val dirNameErrno = fd_prestat_dir_name(fd, bufPointer.address.toInt(), size + 1) + if (dirNameErrno != 0) throw ErrnoException(dirNameErrno.toShort()) + val dirName = bufPointer.readString(size) + map[dirName.toPath()] = fd + } + + return@run map } } + private val pathSegmentsToPreopen: Map, Int> = + pathToPreopen.mapKeys { (key, _) -> key.segmentsBytes } + private val relativePathPreopen: Int = pathToPreopen.values.firstOrNull() + ?: throw IllegalStateException("no preopens") + override fun canonicalize(path: Path): Path { // There's no APIs in preview1 to canonicalize a path. We give it a best effort by resolving // all symlinks, but this could result in a relative path. @@ -126,14 +153,9 @@ class WasiFileSystem( ) when (errno) { - Errno.notcapable.ordinal -> { - // Both of these paths return `notcapable`: - // * The root, '/', which is a parent of real paths. - // * Non-existent paths like '/127.0.0.1/../localhost/c$/Windows', which don't matter. - // Treat the root path as special. - if (path.isRoot) return FileMetadata(isDirectory = true) - return null - } + // 'notcapable' means our preopens don't cover this path. This will happen for paths + // like '/' that are an ancestor of our preopens. + Errno.notcapable.ordinal -> return FileMetadata(isDirectory = true) Errno.noent.ordinal -> return null } @@ -450,30 +472,27 @@ class WasiFileSystem( } /** - * Returns the file descriptor of the preopened path that is an ancestor of [path]. Returns null - * if there is no such file descriptor. + * Returns the file descriptor of the preopened path that is either an ancestor of [path], or that + * [path] is an ancestor of. + * + * If [path] is an ancestor of our preopen, then operating on the path will ultimately fail with a + * `notcapable` errno. */ private fun preopenFd(path: Path): fd? { if (path.isRelative) return relativePathPreopen val pathSegmentsBytes = path.segmentsBytes + + preopens@ for ((candidate, fd) in pathSegmentsToPreopen) { - if (pathSegmentsBytes.size < candidate.size) continue - if (pathSegmentsBytes.subList(0, candidate.size) != candidate) continue + val commonSize = minOf(pathSegmentsBytes.size, candidate.size) + for (i in 0 until commonSize) { + if (pathSegmentsBytes[i] != candidate[i]) continue@preopens + } return fd } return null } override fun toString() = "okio.WasiFileSystem" - - companion object { - /** - * File descriptor of the first preopen in the `WASI` instance's configured `preopens` property. - * This is 3 by default, assuming `stdin` is 0, `stdout` is 1, and `stderr` is 2. - * - * Other preopens are assigned sequentially starting at this value. - */ - val DEFAULT_FIRST_PREOPEN = 3 - } } diff --git a/okio-wasifilesystem/src/wasmMain/kotlin/okio/internal/preview1/Preview1.kt b/okio-wasifilesystem/src/wasmMain/kotlin/okio/internal/preview1/Preview1.kt index 07da60373b..4441d5ec14 100644 --- a/okio-wasifilesystem/src/wasmMain/kotlin/okio/internal/preview1/Preview1.kt +++ b/okio-wasifilesystem/src/wasmMain/kotlin/okio/internal/preview1/Preview1.kt @@ -216,6 +216,29 @@ internal external fun fd_pread( returnPointer: PointerU8, ): Int // should be Short?? +/** + * fd_prestat_dir_name(fd: fd, path: Pointer, path_len: size) -> Result<(), errno> + * + * Return a description of the given preopened file descriptor. + */ +@WasmImport("wasi_snapshot_preview1", "fd_prestat_dir_name") +internal external fun fd_prestat_dir_name( + fd: fd, + path: PointerU8, + pathSize: size, +): Int // should be Short?? + +/** + * fd_prestat_get(fd: fd) -> Result + * + * Return a description of the given preopened file descriptor. + */ +@WasmImport("wasi_snapshot_preview1", "fd_prestat_get") +internal external fun fd_prestat_get( + fd: fd, + returnPointer: PointerU8, +): Int // should be Short?? + /** * fd_pwrite(fd: fd, iovs: ciovec_array, offset: filesize) -> Result` * diff --git a/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemPreopensTest.kt b/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemPreopensTest.kt index 9a2bb38188..ed844fa5cc 100644 --- a/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemPreopensTest.kt +++ b/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemPreopensTest.kt @@ -21,7 +21,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull import okio.Path.Companion.toPath -import okio.WasiFileSystem.Companion.DEFAULT_FIRST_PREOPEN /** * Confirm the [WasiFileSystem] can operate on different preopened directories independently. @@ -29,15 +28,7 @@ import okio.WasiFileSystem.Companion.DEFAULT_FIRST_PREOPEN * This tracks the `preopens` attribute in `.mjs` script in `okio-wasifilesystem/build.gradle.kts`. */ class WasiFileSystemPreopensTest { - private val fileSystem = WasiFileSystem( - relativePathPreopen = DEFAULT_FIRST_PREOPEN, - pathToPreopen = mapOf( - "/tmp".toPath() to DEFAULT_FIRST_PREOPEN, - "/a".toPath() to DEFAULT_FIRST_PREOPEN + 1, - "/b".toPath() to DEFAULT_FIRST_PREOPEN + 2, - ), - ) - + private val fileSystem = WasiFileSystem private val testId = "${this::class.simpleName}-${randomToken(16)}" private val baseA: Path = "/a".toPath() / testId private val baseB: Path = "/b".toPath() / testId diff --git a/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemTest.kt b/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemTest.kt index 7296845ff3..b6846b45d0 100644 --- a/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemTest.kt +++ b/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemTest.kt @@ -19,7 +19,7 @@ import okio.Path.Companion.toPath class WasiFileSystemTest : AbstractFileSystemTest( clock = WasiClock, - fileSystem = WasiFileSystem(), + fileSystem = WasiFileSystem, windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\", allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\", allowAtomicMoveFromFileToDirectory = false, diff --git a/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiTest.kt b/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiTest.kt index e2b69cd15d..6992cb2c3f 100644 --- a/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiTest.kt +++ b/okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiTest.kt @@ -24,7 +24,7 @@ import okio.ByteString.Companion.encodeUtf8 import okio.Path.Companion.toPath class WasiTest { - private val fileSystem = WasiFileSystem() + private val fileSystem = WasiFileSystem private val base: Path = "/tmp".toPath() / "${this::class.simpleName}-${randomToken(16)}" @BeforeTest