Skip to content

Commit c7e7ad3

Browse files
authored
Add API to choose non-default preopens (#1317)
I need to find a proper document explaining how this all works. As is it works but is not following a proper spec.
1 parent 0aac566 commit c7e7ad3

File tree

6 files changed

+171
-35
lines changed

6 files changed

+171
-35
lines changed

Diff for: okio-wasifilesystem/build.gradle.kts

+9-3
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ val injectWasiInit by tasks.creating {
6464
outputs.file(entryPointMjs)
6565

6666
doLast {
67-
val tmpdir = File(System.getProperty("java.io.tmpdir"), "okio-wasifilesystem-test")
68-
tmpdir.mkdirs()
67+
val base = File(System.getProperty("java.io.tmpdir"), "okio-wasifilesystem-test")
68+
val baseA = File(base, "a")
69+
val baseB = File(base, "b")
70+
base.mkdirs()
71+
baseA.mkdirs()
72+
baseB.mkdirs()
6973

7074
entryPointMjs.writeText(
7175
"""
@@ -75,7 +79,9 @@ val injectWasiInit by tasks.creating {
7579
export const wasi = new WASI({
7680
version: 'preview1',
7781
preopens: {
78-
'/tmp': '$tmpdir'
82+
'/tmp': '$base',
83+
'/a': '$baseA',
84+
'/b': '$baseB'
7985
}
8086
});
8187

Diff for: okio-wasifilesystem/src/wasmMain/kotlin/okio/WasiFileSystem.kt

+57-19
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import okio.Path.Companion.toPath
2121
import okio.internal.ErrnoException
2222
import okio.internal.fdClose
2323
import okio.internal.preview1.Errno
24-
import okio.internal.preview1.FirstPreopenDirectoryTmp
2524
import okio.internal.preview1.dirnamelen
2625
import okio.internal.preview1.fd
2726
import okio.internal.preview1.fd_readdir
@@ -60,7 +59,18 @@ import okio.internal.write
6059
*
6160
* [WASI]: https://wasi.dev/
6261
*/
63-
object WasiFileSystem : FileSystem() {
62+
class WasiFileSystem(
63+
private val relativePathPreopen: Int = DEFAULT_FIRST_PREOPEN,
64+
pathToPreopen: Map<Path, Int> = mapOf("/".toPath() to DEFAULT_FIRST_PREOPEN),
65+
) : FileSystem() {
66+
private val pathSegmentsToPreopen = pathToPreopen.mapKeys { (key, _) -> key.segmentsBytes }
67+
68+
init {
69+
require(pathSegmentsToPreopen.isNotEmpty()) {
70+
"pathToPreopen must be non-empty"
71+
}
72+
}
73+
6474
override fun canonicalize(path: Path): Path {
6575
// There's no APIs in preview1 to canonicalize a path. We give it a best effort by resolving
6676
// all symlinks, but this could result in a relative path.
@@ -108,7 +118,7 @@ object WasiFileSystem : FileSystem() {
108118
val (pathAddress, pathSize) = allocator.write(path.toString())
109119

110120
val errno = path_filestat_get(
111-
fd = FirstPreopenDirectoryTmp,
121+
fd = preopenFd(path) ?: return null,
112122
flags = 0,
113123
path = pathAddress.address.toInt(),
114124
pathSize = pathSize,
@@ -144,7 +154,7 @@ object WasiFileSystem : FileSystem() {
144154
val bufPointer = allocator.allocate(bufLen)
145155
val readlinkReturnPointer = allocator.allocate(4) // `size` is u32, 4 bytes.
146156
val readlinkErrno = path_readlink(
147-
fd = FirstPreopenDirectoryTmp,
157+
fd = preopenFd(path) ?: return null,
148158
path = pathAddress.address.toInt(),
149159
pathSize = pathSize,
150160
buf = bufPointer.address.toInt(),
@@ -174,7 +184,7 @@ object WasiFileSystem : FileSystem() {
174184

175185
override fun list(dir: Path): List<Path> {
176186
val fd = pathOpen(
177-
path = dir.toString(),
187+
path = dir,
178188
oflags = oflag_directory,
179189
rightsBase = right_fd_readdir,
180190
)
@@ -252,7 +262,7 @@ object WasiFileSystem : FileSystem() {
252262
right_fd_seek or
253263
right_fd_sync
254264
val fd = pathOpen(
255-
path = file.toString(),
265+
path = file,
256266
oflags = 0,
257267
rightsBase = rightsBase,
258268
)
@@ -275,7 +285,7 @@ object WasiFileSystem : FileSystem() {
275285
right_fd_sync or
276286
right_fd_write
277287
val fd = pathOpen(
278-
path = file.toString(),
288+
path = file,
279289
oflags = oflags,
280290
rightsBase = rightsBase,
281291
)
@@ -285,7 +295,7 @@ object WasiFileSystem : FileSystem() {
285295
override fun source(file: Path): Source {
286296
return FileSource(
287297
fd = pathOpen(
288-
path = file.toString(),
298+
path = file,
289299
oflags = 0,
290300
rightsBase = right_fd_read,
291301
),
@@ -300,7 +310,7 @@ object WasiFileSystem : FileSystem() {
300310

301311
return FileSink(
302312
fd = pathOpen(
303-
path = file.toString(),
313+
path = file,
304314
oflags = oflags,
305315
rightsBase = right_fd_write or right_fd_sync,
306316
),
@@ -315,7 +325,7 @@ object WasiFileSystem : FileSystem() {
315325

316326
return FileSink(
317327
fd = pathOpen(
318-
path = file.toString(),
328+
path = file,
319329
oflags = oflags,
320330
rightsBase = right_fd_write,
321331
fdflags = fdflags_append,
@@ -328,7 +338,7 @@ object WasiFileSystem : FileSystem() {
328338
val (pathAddress, pathSize) = allocator.write(dir.toString())
329339

330340
val errno = path_create_directory(
331-
fd = FirstPreopenDirectoryTmp,
341+
fd = preopenFd(dir) ?: throw FileNotFoundException("no preopen: $dir"),
332342
path = pathAddress.address.toInt(),
333343
pathSize = pathSize,
334344
)
@@ -347,10 +357,10 @@ object WasiFileSystem : FileSystem() {
347357
val (targetPathAddress, targetPathSize) = allocator.write(target.toString())
348358

349359
val errno = path_rename(
350-
fd = FirstPreopenDirectoryTmp,
360+
fd = preopenFd(source) ?: throw FileNotFoundException("no preopen: $source"),
351361
old_path = sourcePathAddress.address.toInt(),
352362
old_pathSize = sourcePathSize,
353-
new_fd = FirstPreopenDirectoryTmp,
363+
new_fd = preopenFd(target) ?: throw FileNotFoundException("no preopen: $target"),
354364
new_path = targetPathAddress.address.toInt(),
355365
new_pathSize = targetPathSize,
356366
)
@@ -365,9 +375,10 @@ object WasiFileSystem : FileSystem() {
365375
override fun delete(path: Path, mustExist: Boolean) {
366376
withScopedMemoryAllocator { allocator ->
367377
val (pathAddress, pathSize) = allocator.write(path.toString())
378+
val preopenFd = preopenFd(path) ?: throw FileNotFoundException("no preopen: $path")
368379

369380
var errno = path_unlink_file(
370-
fd = FirstPreopenDirectoryTmp,
381+
fd = preopenFd,
371382
path = pathAddress.address.toInt(),
372383
pathSize = pathSize,
373384
)
@@ -382,7 +393,7 @@ object WasiFileSystem : FileSystem() {
382393
Errno.isdir.ordinal,
383394
-> {
384395
errno = path_remove_directory(
385-
fd = FirstPreopenDirectoryTmp,
396+
fd = preopenFd,
386397
path = pathAddress.address.toInt(),
387398
pathSize = pathSize,
388399
)
@@ -400,7 +411,7 @@ object WasiFileSystem : FileSystem() {
400411
val errno = path_symlink(
401412
old_path = targetPathAddress.address.toInt(),
402413
old_pathSize = targetPathSize,
403-
fd = FirstPreopenDirectoryTmp,
414+
fd = preopenFd(source) ?: throw FileNotFoundException("no preopen: $source"),
404415
new_path = sourcePathAddress.address.toInt(),
405416
new_pathSize = sourcePathSize,
406417
)
@@ -409,17 +420,18 @@ object WasiFileSystem : FileSystem() {
409420
}
410421

411422
private fun pathOpen(
412-
path: String,
423+
path: Path,
413424
oflags: oflags,
414425
rightsBase: rights,
415426
fdflags: fdflags = 0,
416427
): fd {
417428
withScopedMemoryAllocator { allocator ->
418-
val (pathAddress, pathSize) = allocator.write(path)
429+
val preopenFd = preopenFd(path) ?: throw FileNotFoundException("no preopen: $path")
430+
val (pathAddress, pathSize) = allocator.write(path.toString())
419431

420432
val returnPointer: Pointer = allocator.allocate(4) // fd is u32.
421433
val errno = path_open(
422-
fd = FirstPreopenDirectoryTmp,
434+
fd = preopenFd,
423435
dirflags = 0,
424436
path = pathAddress.address.toInt(),
425437
pathSize = pathSize,
@@ -437,5 +449,31 @@ object WasiFileSystem : FileSystem() {
437449
}
438450
}
439451

452+
/**
453+
* Returns the file descriptor of the preopened path that is an ancestor of [path]. Returns null
454+
* if there is no such file descriptor.
455+
*/
456+
private fun preopenFd(path: Path): fd? {
457+
if (path.isRelative) return relativePathPreopen
458+
459+
val pathSegmentsBytes = path.segmentsBytes
460+
for ((candidate, fd) in pathSegmentsToPreopen) {
461+
if (pathSegmentsBytes.size < candidate.size) continue
462+
if (pathSegmentsBytes.subList(0, candidate.size) != candidate) continue
463+
return fd
464+
}
465+
return null
466+
}
467+
440468
override fun toString() = "okio.WasiFileSystem"
469+
470+
companion object {
471+
/**
472+
* File descriptor of the first preopen in the `WASI` instance's configured `preopens` property.
473+
* This is 3 by default, assuming `stdin` is 0, `stdout` is 1, and `stderr` is 2.
474+
*
475+
* Other preopens are assigned sequentially starting at this value.
476+
*/
477+
val DEFAULT_FIRST_PREOPEN = 3
478+
}
441479
}

Diff for: okio-wasifilesystem/src/wasmMain/kotlin/okio/internal/preview1/Preview1.kt

-11
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,6 @@ typealias dirnamelen = Int
5252
*/
5353
typealias PointerU8 = Int
5454

55-
val Stdin: fd = 0
56-
val Stdout: fd = 1
57-
val Stderr: fd = 2
58-
59-
/**
60-
* Assume the /tmp directory is fd 3.
61-
*
62-
* TODO: look this up at runtime from whatever parent directory is requested.
63-
*/
64-
val FirstPreopenDirectoryTmp: fd = 3
65-
6655
/**
6756
* path_create_directory(fd: fd, path: string) -> Result<(), errno>
6857
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (C) 2023 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package okio
17+
18+
import kotlin.test.BeforeTest
19+
import kotlin.test.Test
20+
import kotlin.test.assertEquals
21+
import kotlin.test.assertFailsWith
22+
import kotlin.test.assertNull
23+
import okio.Path.Companion.toPath
24+
import okio.WasiFileSystem.Companion.DEFAULT_FIRST_PREOPEN
25+
26+
/**
27+
* Confirm the [WasiFileSystem] can operate on different preopened directories independently.
28+
*
29+
* This tracks the `preopens` attribute in `.mjs` script in `okio-wasifilesystem/build.gradle.kts`.
30+
*/
31+
class WasiFileSystemPreopensTest {
32+
private val fileSystem = WasiFileSystem(
33+
relativePathPreopen = DEFAULT_FIRST_PREOPEN,
34+
pathToPreopen = mapOf(
35+
"/tmp".toPath() to DEFAULT_FIRST_PREOPEN,
36+
"/a".toPath() to DEFAULT_FIRST_PREOPEN + 1,
37+
"/b".toPath() to DEFAULT_FIRST_PREOPEN + 2,
38+
),
39+
)
40+
41+
private val testId = "${this::class.simpleName}-${randomToken(16)}"
42+
private val baseA: Path = "/a".toPath() / testId
43+
private val baseB: Path = "/b".toPath() / testId
44+
45+
@BeforeTest
46+
fun setUp() {
47+
fileSystem.createDirectory(baseA)
48+
fileSystem.createDirectory(baseB)
49+
}
50+
51+
@Test
52+
fun operateOnPreopens() {
53+
fileSystem.write(baseA / "a.txt") {
54+
writeUtf8("hello world a")
55+
}
56+
fileSystem.write(baseB / "b.txt") {
57+
writeUtf8("bello burld")
58+
}
59+
assertEquals(
60+
"hello world a".length.toLong(),
61+
fileSystem.metadata(baseA / "a.txt").size,
62+
)
63+
assertEquals(
64+
"bello burld".length.toLong(),
65+
fileSystem.metadata(baseB / "b.txt").size,
66+
)
67+
}
68+
69+
@Test
70+
fun operateAcrossPreopens() {
71+
fileSystem.write(baseA / "a.txt") {
72+
writeUtf8("hello world")
73+
}
74+
75+
fileSystem.atomicMove(baseA / "a.txt", baseB / "b.txt")
76+
77+
assertEquals(
78+
"hello world",
79+
fileSystem.read(baseB / "b.txt") {
80+
readUtf8()
81+
},
82+
)
83+
}
84+
85+
@Test
86+
fun cannotOperateOutsideOfPreopens() {
87+
val noPreopen = "/c".toPath() / testId
88+
assertFailsWith<FileNotFoundException> {
89+
fileSystem.createDirectory(noPreopen)
90+
}
91+
assertFailsWith<FileNotFoundException> {
92+
fileSystem.sink(noPreopen)
93+
}
94+
assertNull(fileSystem.metadataOrNull(noPreopen))
95+
assertFailsWith<FileNotFoundException> {
96+
fileSystem.metadata(noPreopen)
97+
}
98+
assertNull(fileSystem.listOrNull(noPreopen))
99+
assertFailsWith<FileNotFoundException> {
100+
fileSystem.list(noPreopen)
101+
}
102+
}
103+
}

Diff for: okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiFileSystemTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import okio.Path.Companion.toPath
1919

2020
class WasiFileSystemTest : AbstractFileSystemTest(
2121
clock = WasiClock,
22-
fileSystem = WasiFileSystem,
22+
fileSystem = WasiFileSystem(),
2323
windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\",
2424
allowClobberingEmptyDirectories = Path.DIRECTORY_SEPARATOR == "\\",
2525
allowAtomicMoveFromFileToDirectory = false,

Diff for: okio-wasifilesystem/src/wasmTest/kotlin/okio/WasiTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import okio.ByteString.Companion.encodeUtf8
2424
import okio.Path.Companion.toPath
2525

2626
class WasiTest {
27-
private val fileSystem = WasiFileSystem
27+
private val fileSystem = WasiFileSystem()
2828
private val base: Path = "/tmp".toPath() / "${this::class.simpleName}-${randomToken(16)}"
2929

3030
@BeforeTest

0 commit comments

Comments
 (0)