Skip to content

Commit

Permalink
Only create relative symlinks in WasiFileSystem
Browse files Browse the repository at this point in the history
Creating absolute symlinks is broken in WASI. This also works
better for preopens.
  • Loading branch information
squarejesse committed Aug 1, 2023
1 parent 094fd45 commit b900105
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,6 @@ abstract class AbstractFileSystemTest(
@Test
fun listRecursivelyFollowsSymlinks() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "a"
val baseAA = baseA / "a"
Expand Down Expand Up @@ -639,7 +638,6 @@ abstract class AbstractFileSystemTest(
@Test
fun listRecursivelyOnSymlink() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "a"
val baseAA = baseA / "a"
Expand Down Expand Up @@ -673,7 +671,6 @@ abstract class AbstractFileSystemTest(
@Test
fun listRecursiveOnSymlinkWithSpecialCharacterNamedFiles() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "ä"
val baseASuperSaiyan = baseA / "超サイヤ人"
Expand All @@ -691,7 +688,6 @@ abstract class AbstractFileSystemTest(
@Test
fun listRecursivelyOnSymlinkCycleThrows() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "a"
val baseAB = baseA / "b"
Expand Down Expand Up @@ -2311,7 +2307,14 @@ abstract class AbstractFileSystemTest(
val maxTime = clock.now()

val sourceMetadata = fileSystem.metadata(source)
assertEquals(target, sourceMetadata.symlinkTarget)
// Okio's WasiFileSystem only creates relative symlinks.
assertEquals(
when {
isWasiFileSystem -> target.relativeTo(source.parent!!)
else -> target
},
sourceMetadata.symlinkTarget,
)
assertInRange(sourceMetadata.createdAt, minTime, maxTime)
}

Expand Down Expand Up @@ -2359,7 +2362,6 @@ abstract class AbstractFileSystemTest(
@Test
fun openSymlinkSource() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val target = base / "symlink-target"
val source = base / "symlink-source"
Expand All @@ -2373,7 +2375,6 @@ abstract class AbstractFileSystemTest(
fun openSymlinkSink() {
if (!supportsSymlink()) return
if (isJimFileSystem()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val target = base / "symlink-target"
val source = base / "symlink-source"
Expand All @@ -2387,7 +2388,6 @@ abstract class AbstractFileSystemTest(
@Test
fun openFileWithDirectorySymlink() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "a"
val baseAA = base / "a" / "a"
Expand All @@ -2403,7 +2403,6 @@ abstract class AbstractFileSystemTest(
@Test
fun openSymlinkFileHandle() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val target = base / "symlink-target"
val source = base / "symlink-source"
Expand All @@ -2418,7 +2417,6 @@ abstract class AbstractFileSystemTest(
@Test
fun listSymlinkDirectory() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "a"
val baseAA = base / "a" / "a"
Expand All @@ -2436,7 +2434,6 @@ abstract class AbstractFileSystemTest(
@Test
fun symlinkFileLastAccessedAt() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val target = base / "symlink-target"
val source = base / "symlink-source"
Expand All @@ -2452,7 +2449,6 @@ abstract class AbstractFileSystemTest(
@Test
fun symlinkDirectoryLastAccessedAt() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val baseA = base / "a"
val baseAA = base / "a" / "a"
Expand Down Expand Up @@ -2483,7 +2479,6 @@ abstract class AbstractFileSystemTest(
@Test
fun moveSymlinkDoesntMoveTargetFile() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val target = base / "symlink-target"
val source1 = base / "symlink-source-1"
Expand All @@ -2493,7 +2488,14 @@ abstract class AbstractFileSystemTest(
fileSystem.atomicMove(source1, source2)
assertEquals("I am the target file", target.readUtf8())
assertEquals("I am the target file", source2.readUtf8())
assertEquals(target, fileSystem.metadata(source2).symlinkTarget)
// Okio's WasiFileSystem only creates relative symlinks.
assertEquals(
when {
isWasiFileSystem -> target.relativeTo(source1.parent!!)
else -> target
},
fileSystem.metadata(source2).symlinkTarget,
)
}

@Test
Expand Down Expand Up @@ -2525,7 +2527,6 @@ abstract class AbstractFileSystemTest(
@Test
fun followingRecursiveSymlinksIsOkay() {
if (!supportsSymlink()) return
if (isWasiFileSystem) return // Symlinks to absolute paths are broken on WASI.

val pathA = base / "symlink-a"
val pathB = base / "symlink-b"
Expand Down
72 changes: 40 additions & 32 deletions okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,13 @@ import okio.internal.write
* [WASI]: https://wasi.dev/
*/
object WasiFileSystem : FileSystem() {
private val pathToPreopen: Map<Path, Int> = run {
private val preopens: List<Preopen> = buildList {
// 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)

Expand All @@ -86,16 +84,13 @@ object WasiFileSystem : FileSystem() {
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
val dirNamePath = dirName.toPath()
add(Preopen(dirNamePath.segmentsBytes, fd))
}

return@run map
}
}

private val pathSegmentsToPreopen: Map<List<ByteString>, Int> =
pathToPreopen.mapKeys { (key, _) -> key.segmentsBytes }
private val relativePathPreopen: Int = pathToPreopen.values.firstOrNull()
private val relativePathPreopen: Preopen = preopens.firstOrNull()
?: throw IllegalStateException("no preopens")

override fun canonicalize(path: Path): Path {
Expand Down Expand Up @@ -142,10 +137,11 @@ object WasiFileSystem : FileSystem() {
override fun metadataOrNull(path: Path): FileMetadata? {
withScopedMemoryAllocator { allocator ->
val returnPointer = allocator.allocate(64)
val preopen = preopenForPath(path) ?: return null
val (pathAddress, pathSize) = allocator.write(path.toString())

val errno = path_filestat_get(
fd = preopenFd(path) ?: return null,
fd = preopen.fd,
flags = 0,
path = pathAddress.address.toInt(),
pathSize = pathSize,
Expand Down Expand Up @@ -176,7 +172,7 @@ object WasiFileSystem : FileSystem() {
val bufPointer = allocator.allocate(bufLen)
val readlinkReturnPointer = allocator.allocate(4) // `size` is u32, 4 bytes.
val readlinkErrno = path_readlink(
fd = preopenFd(path) ?: return null,
fd = preopen.fd,
path = pathAddress.address.toInt(),
pathSize = pathSize,
buf = bufPointer.address.toInt(),
Expand Down Expand Up @@ -360,7 +356,7 @@ object WasiFileSystem : FileSystem() {
val (pathAddress, pathSize) = allocator.write(dir.toString())

val errno = path_create_directory(
fd = preopenFd(dir) ?: throw FileNotFoundException("no preopen: $dir"),
fd = preopenForPath(dir)?.fd ?: throw FileNotFoundException("no preopen: $dir"),
path = pathAddress.address.toInt(),
pathSize = pathSize,
)
Expand All @@ -379,10 +375,10 @@ object WasiFileSystem : FileSystem() {
val (targetPathAddress, targetPathSize) = allocator.write(target.toString())

val errno = path_rename(
fd = preopenFd(source) ?: throw FileNotFoundException("no preopen: $source"),
fd = preopenForPath(source)?.fd ?: throw FileNotFoundException("no preopen: $source"),
old_path = sourcePathAddress.address.toInt(),
old_pathSize = sourcePathSize,
new_fd = preopenFd(target) ?: throw FileNotFoundException("no preopen: $target"),
new_fd = preopenForPath(target)?.fd ?: throw FileNotFoundException("no preopen: $target"),
new_path = targetPathAddress.address.toInt(),
new_pathSize = targetPathSize,
)
Expand All @@ -397,10 +393,10 @@ object WasiFileSystem : FileSystem() {
override fun delete(path: Path, mustExist: Boolean) {
withScopedMemoryAllocator { allocator ->
val (pathAddress, pathSize) = allocator.write(path.toString())
val preopenFd = preopenFd(path) ?: throw FileNotFoundException("no preopen: $path")
val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path")

var errno = path_unlink_file(
fd = preopenFd,
fd = preopenFd.fd,
path = pathAddress.address.toInt(),
pathSize = pathSize,
)
Expand All @@ -415,7 +411,7 @@ object WasiFileSystem : FileSystem() {
Errno.isdir.ordinal,
-> {
errno = path_remove_directory(
fd = preopenFd,
fd = preopenFd.fd,
path = pathAddress.address.toInt(),
pathSize = pathSize,
)
Expand All @@ -427,13 +423,25 @@ object WasiFileSystem : FileSystem() {

override fun createSymlink(source: Path, target: Path) {
withScopedMemoryAllocator { allocator ->
val sourcePreopen = preopenForPath(source)
?: throw FileNotFoundException("no preopen: $source")

// Always create symlinks relative to their source. Absolute symlinks are trouble because the
// absolute paths used by WASI are different from the absolute paths on the host file system.
val sourceParent = source.parent
?: throw IOException("unexpected symlink source: $source")
val targetRelative = when {
target.isRelative -> target
else -> target.relativeTo(sourceParent)
}

val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString())
val (targetPathAddress, targetPathSize) = allocator.write(target.toString())
val (targetPathAddress, targetPathSize) = allocator.write(targetRelative.toString())

val errno = path_symlink(
old_path = targetPathAddress.address.toInt(),
old_pathSize = targetPathSize,
fd = preopenFd(source) ?: throw FileNotFoundException("no preopen: $source"),
fd = sourcePreopen.fd,
new_path = sourcePathAddress.address.toInt(),
new_pathSize = sourcePathSize,
)
Expand All @@ -448,12 +456,12 @@ object WasiFileSystem : FileSystem() {
fdflags: fdflags = 0,
): fd {
withScopedMemoryAllocator { allocator ->
val preopenFd = preopenFd(path) ?: throw FileNotFoundException("no preopen: $path")
val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path")
val (pathAddress, pathSize) = allocator.write(path.toString())

val returnPointer: Pointer = allocator.allocate(4) // fd is u32.
val errno = path_open(
fd = preopenFd,
fd = preopenFd.fd,
dirflags = 0,
path = pathAddress.address.toInt(),
pathSize = pathSize,
Expand All @@ -472,27 +480,27 @@ object WasiFileSystem : FileSystem() {
}

/**
* Returns the file descriptor of the preopened path that is either an ancestor of [path], or that
* [path] is an ancestor of.
* Returns the preopen whose path is either an ancestor of [path], or whose path [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? {
private fun preopenForPath(path: Path): Preopen? {
if (path.isRelative) return relativePathPreopen

val pathSegmentsBytes = path.segmentsBytes

preopens@
for ((candidate, fd) in pathSegmentsToPreopen) {
val commonSize = minOf(pathSegmentsBytes.size, candidate.size)
for (i in 0 until commonSize) {
if (pathSegmentsBytes[i] != candidate[i]) continue@preopens
}
return fd
return preopens.firstOrNull { preopen ->
val commonSize = minOf(pathSegmentsBytes.size, preopen.segmentsBytes.size)
preopen.segmentsBytes.subList(0, commonSize) == pathSegmentsBytes.subList(0, commonSize)
}
return null
}

override fun toString() = "okio.WasiFileSystem"

private class Preopen(
val segmentsBytes: List<ByteString>,
val fd: fd,
)
}

0 comments on commit b900105

Please sign in to comment.