Skip to content

Commit

Permalink
Automatically get preopens via the API (#1319)
Browse files Browse the repository at this point in the history
  • Loading branch information
squarejesse authored Jul 31, 2023
1 parent 3e30feb commit 094fd45
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 43 deletions.
81 changes: 50 additions & 31 deletions okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,18 +61,43 @@ import okio.internal.write
*
* [WASI]: https://wasi.dev/
*/
class WasiFileSystem(
private val relativePathPreopen: Int = DEFAULT_FIRST_PREOPEN,
pathToPreopen: Map<Path, Int> = 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<Path, Int> = 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<Path, Int>()

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<List<ByteString>, 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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,29 @@ internal external fun fd_pread(
returnPointer: PointerU8,
): Int // should be Short??

/**
* fd_prestat_dir_name(fd: fd, path: Pointer<u8>, 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<prestat, errno>
*
* 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<size, errno>`
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,14 @@ 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.
*
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 094fd45

Please sign in to comment.