xref: /aosp_15_r20/external/okio/okio-wasifilesystem/src/wasmWasiMain/kotlin/okio/WasiFileSystem.kt (revision f9742813c14b702d71392179818a9e591da8620c)
1 /*
<lambda>null2  * 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.wasm.unsafe.Pointer
19 import kotlin.wasm.unsafe.withScopedMemoryAllocator
20 import okio.Path.Companion.toPath
21 import okio.internal.ErrnoException
22 import okio.internal.fdClose
23 import okio.internal.preview1.Errno
24 import okio.internal.preview1.dirnamelen
25 import okio.internal.preview1.fd
26 import okio.internal.preview1.fd_prestat_dir_name
27 import okio.internal.preview1.fd_prestat_get
28 import okio.internal.preview1.fd_readdir
29 import okio.internal.preview1.fdflags
30 import okio.internal.preview1.fdflags_append
31 import okio.internal.preview1.filetype
32 import okio.internal.preview1.filetype_directory
33 import okio.internal.preview1.filetype_regular_file
34 import okio.internal.preview1.filetype_symbolic_link
35 import okio.internal.preview1.oflag_creat
36 import okio.internal.preview1.oflag_directory
37 import okio.internal.preview1.oflag_excl
38 import okio.internal.preview1.oflag_trunc
39 import okio.internal.preview1.oflags
40 import okio.internal.preview1.path_create_directory
41 import okio.internal.preview1.path_filestat_get
42 import okio.internal.preview1.path_open
43 import okio.internal.preview1.path_readlink
44 import okio.internal.preview1.path_remove_directory
45 import okio.internal.preview1.path_rename
46 import okio.internal.preview1.path_symlink
47 import okio.internal.preview1.path_unlink_file
48 import okio.internal.preview1.right_fd_filestat_get
49 import okio.internal.preview1.right_fd_filestat_set_size
50 import okio.internal.preview1.right_fd_read
51 import okio.internal.preview1.right_fd_readdir
52 import okio.internal.preview1.right_fd_seek
53 import okio.internal.preview1.right_fd_sync
54 import okio.internal.preview1.right_fd_write
55 import okio.internal.preview1.rights
56 import okio.internal.readString
57 import okio.internal.write
58 
59 /**
60  * Use [WASI] to implement the Okio file system interface.
61  *
62  * [WASI]: https://wasi.dev/
63  */
64 object WasiFileSystem : FileSystem() {
65   private val preopens: List<Preopen> = buildList {
66     // File descriptor of the first preopen in the `WASI` instance's configured `preopens` property.
67     // This is 3 by default, assuming `stdin` is 0, `stdout` is 1, and `stderr` is 2. Other preopens
68     // are assigned sequentially starting at this value.
69     val firstPreopen = 3
70 
71     withScopedMemoryAllocator { allocator ->
72       val bufSize = 2048
73       val bufPointer = allocator.allocate(bufSize)
74 
75       for (fd in firstPreopen..Int.MAX_VALUE) {
76         val getReturnPointer = allocator.allocate(12)
77 
78         val getErrno = fd_prestat_get(fd, getReturnPointer.address.toInt())
79         if (getErrno == Errno.badf.ordinal) break // No more preopens.
80         if (getErrno != 0) throw ErrnoException(getErrno.toShort())
81 
82         val size = (getReturnPointer + 4).loadInt()
83         require(size + 1 < bufSize) { "unexpected preopen size: $size" }
84         val dirNameErrno = fd_prestat_dir_name(fd, bufPointer.address.toInt(), size + 1)
85         if (dirNameErrno != 0) throw ErrnoException(dirNameErrno.toShort())
86         val dirName = bufPointer.readString(size)
87         val dirNamePath = dirName.toPath()
88         add(Preopen(dirNamePath, dirNamePath.segmentsBytes, fd))
89       }
90     }
91   }
92 
93   private val relativePathPreopen: Preopen = preopens.firstOrNull()
94     ?: throw IllegalStateException("no preopens")
95 
96   override fun canonicalize(path: Path): Path {
97     val absolutePath = when {
98       path.isAbsolute -> path
99       else -> relativePathPreopen.path.resolve(path, normalize = true)
100     }
101 
102     // There's no APIs in preview1 to canonicalize a path. We give it a best effort by resolving
103     // all symlinks, but this could result in a relative path.
104     val result = resolveSymlinks(absolutePath, 0).normalized()
105 
106     check(result.isAbsolute) {
107       "Canonicalize $path returned non-absolute path: $result"
108     }
109 
110     return result
111   }
112 
113   private fun resolveSymlinks(
114     path: Path,
115     recurseCount: Int = 0,
116   ): Path {
117     // 40 is chosen for consistency with the Linux kernel (which previously used 8).
118     if (recurseCount > 40) throw IOException("symlink cycle?")
119 
120     val parent = path.parent
121     val resolvedParent = when {
122       parent != null -> resolveSymlinks(parent, recurseCount + 1)
123       else -> null
124     }
125     val pathWithResolvedParent = when {
126       resolvedParent != null -> resolvedParent.resolve(path.name)
127       else -> path
128     }
129 
130     val symlinkTarget = metadata(pathWithResolvedParent).symlinkTarget
131       ?: return pathWithResolvedParent
132 
133     val resolvedSymlinkTarget = when {
134       symlinkTarget.isAbsolute -> symlinkTarget
135       resolvedParent != null -> resolvedParent.resolve(symlinkTarget)
136       else -> symlinkTarget
137     }
138 
139     return resolveSymlinks(resolvedSymlinkTarget, recurseCount + 1)
140   }
141 
142   override fun metadataOrNull(path: Path): FileMetadata? {
143     withScopedMemoryAllocator { allocator ->
144       val returnPointer = allocator.allocate(64)
145       val preopen = preopenForPath(path) ?: return null
146       val (pathAddress, pathSize) = allocator.write(path.toString())
147 
148       val errno = path_filestat_get(
149         fd = preopen.fd,
150         flags = 0,
151         path = pathAddress.address.toInt(),
152         pathSize = pathSize,
153         returnPointer = returnPointer.address.toInt(),
154       )
155 
156       when (errno) {
157         // 'notcapable' means our preopens don't cover this path. This will happen for paths
158         // like '/' that are an ancestor of our preopens.
159         Errno.notcapable.ordinal -> return FileMetadata(isDirectory = true)
160         Errno.noent.ordinal -> return null
161       }
162 
163       if (errno != 0) throw ErrnoException(errno.toShort())
164 
165       // Skip device, offset 0.
166       // Skip ino, offset 8.
167       val filetype: filetype = (returnPointer + 16).loadByte()
168       // Skip nlink, offset 24.
169       val filesize: Long = (returnPointer + 32).loadLong()
170       val atim: Long = (returnPointer + 40).loadLong() // Access time, Nanoseconds.
171       val mtim: Long = (returnPointer + 48).loadLong() // Modification time, Nanoseconds.
172       val ctim: Long = (returnPointer + 56).loadLong() // Status change time, Nanoseconds.
173 
174       val symlinkTarget: Path? = when (filetype) {
175         filetype_symbolic_link -> {
176           val bufLen = filesize.toInt() + 1
177           val bufPointer = allocator.allocate(bufLen)
178           val readlinkReturnPointer = allocator.allocate(4) // `size` is u32, 4 bytes.
179           val readlinkErrno = path_readlink(
180             fd = preopen.fd,
181             path = pathAddress.address.toInt(),
182             pathSize = pathSize,
183             buf = bufPointer.address.toInt(),
184             buf_len = bufLen,
185             returnPointer = readlinkReturnPointer.address.toInt(),
186           )
187           if (readlinkErrno != 0) throw ErrnoException(readlinkErrno.toShort())
188           val symlinkSize = readlinkReturnPointer.loadInt()
189           val symlink = bufPointer.readString(symlinkSize)
190           symlink.toPath()
191         }
192 
193         else -> null
194       }
195 
196       return FileMetadata(
197         isRegularFile = filetype == filetype_regular_file,
198         isDirectory = filetype == filetype_directory,
199         symlinkTarget = symlinkTarget,
200         size = filesize,
201         createdAtMillis = ctim / 1_000_000L, // Nanos to millis.
202         lastModifiedAtMillis = mtim / 1_000_000L, // Nanos to millis.
203         lastAccessedAtMillis = atim / 1_000_000L, // Nanos to millis.
204       )
205     }
206   }
207 
208   override fun list(dir: Path): List<Path> {
209     val fd = pathOpen(
210       path = dir,
211       oflags = oflag_directory,
212       rightsBase = right_fd_readdir,
213     )
214     try {
215       return list(dir, fd)
216     } finally {
217       fdClose(fd)
218     }
219   }
220 
221   override fun listOrNull(dir: Path): List<Path>? {
222     // TODO: stop using exceptions for flow control.
223     try {
224       return list(dir)
225     } catch (e: FileNotFoundException) {
226       return null
227     } catch (e: ErrnoException) {
228       if (e.errno == Errno.notdir) return null
229       throw e
230     }
231   }
232 
233   private fun list(dir: Path, fd: fd): List<Path> {
234     withScopedMemoryAllocator { allocator ->
235       // In theory, fd_readdir uses a 'cookie' field to page through results. In practice the
236       // NodeJS implementation doesn't honor the cookie and directories with large file names
237       // don't progress. Instead, just grow the buffer until the entire directory fits.
238       var bufSize = 2048
239       var bufPointer = allocator.allocate(bufSize)
240       val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes.
241       var pageSize: Int
242       while (true) {
243         val errno = fd_readdir(
244           fd = fd,
245           buf = bufPointer.address.toInt(),
246           buf_len = bufSize,
247           cookie = 0L, // Don't bother with dircookie, it doesn't work for large file names.
248           returnPointer = returnPointer.address.toInt(),
249         )
250 
251         if (errno != 0) throw ErrnoException(errno.toShort())
252         pageSize = returnPointer.loadInt()
253 
254         if (pageSize < bufSize) break
255 
256         bufSize *= 4
257         bufPointer = allocator.allocate(bufSize)
258       }
259 
260       // Parse dirent records from the buffer.
261       var pos = bufPointer
262       val limit = bufPointer + pageSize
263       val result = mutableListOf<Path>()
264       while (pos.address < limit.address) {
265         pos += 8 // Skip dircookie.
266         pos += 8 // Skip inode.
267         val d_namelen: dirnamelen = pos.loadInt()
268         pos += 4 // Consume d_namelen.
269         pos += 4 // Skip d_type.
270 
271         val name = pos.readString(d_namelen)
272         pos += d_namelen
273 
274         result += dir / name
275       }
276 
277       result.sort()
278       return result
279     }
280   }
281 
282   override fun openReadOnly(file: Path): FileHandle {
283     val rightsBase = right_fd_filestat_get or
284       right_fd_read or
285       right_fd_seek or
286       right_fd_sync
287     val fd = pathOpen(
288       path = file,
289       oflags = 0,
290       rightsBase = rightsBase,
291     )
292     return WasiFileHandle(fd, readWrite = false)
293   }
294 
295   override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle {
296     val oflags = when {
297       mustCreate && mustExist -> {
298         throw IllegalArgumentException("Cannot require mustCreate and mustExist at the same time.")
299       }
300       mustCreate -> oflag_creat or oflag_excl
301       mustExist -> 0
302       else -> oflag_creat
303     }
304     val rightsBase = right_fd_filestat_get or
305       right_fd_filestat_set_size or
306       right_fd_read or
307       right_fd_seek or
308       right_fd_sync or
309       right_fd_write
310     val fd = pathOpen(
311       path = file,
312       oflags = oflags,
313       rightsBase = rightsBase,
314     )
315     return WasiFileHandle(fd, readWrite = true)
316   }
317 
318   override fun source(file: Path): Source {
319     return FileSource(
320       fd = pathOpen(
321         path = file,
322         oflags = 0,
323         rightsBase = right_fd_read,
324       ),
325     )
326   }
327 
328   override fun sink(file: Path, mustCreate: Boolean): Sink {
329     val oflags = when {
330       mustCreate -> oflag_creat or oflag_excl or oflag_trunc
331       else -> oflag_creat or oflag_trunc
332     }
333 
334     return FileSink(
335       fd = pathOpen(
336         path = file,
337         oflags = oflags,
338         rightsBase = right_fd_write or right_fd_sync,
339       ),
340     )
341   }
342 
343   override fun appendingSink(file: Path, mustExist: Boolean): Sink {
344     val oflags = when {
345       mustExist -> 0
346       else -> oflag_creat
347     }
348 
349     return FileSink(
350       fd = pathOpen(
351         path = file,
352         oflags = oflags,
353         rightsBase = right_fd_write,
354         fdflags = fdflags_append,
355       ),
356     )
357   }
358 
359   override fun createDirectory(dir: Path, mustCreate: Boolean) {
360     withScopedMemoryAllocator { allocator ->
361       val (pathAddress, pathSize) = allocator.write(dir.toString())
362 
363       val errno = path_create_directory(
364         fd = preopenForPath(dir)?.fd ?: throw FileNotFoundException("no preopen: $dir"),
365         path = pathAddress.address.toInt(),
366         pathSize = pathSize,
367       )
368       if (errno == Errno.exist.ordinal) {
369         if (mustCreate) throw IOException("already exists: $dir")
370         return
371       }
372 
373       if (errno != 0) throw ErrnoException(errno.toShort())
374     }
375   }
376 
377   override fun atomicMove(source: Path, target: Path) {
378     withScopedMemoryAllocator { allocator ->
379       val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString())
380       val (targetPathAddress, targetPathSize) = allocator.write(target.toString())
381 
382       val errno = path_rename(
383         fd = preopenForPath(source)?.fd ?: throw FileNotFoundException("no preopen: $source"),
384         old_path = sourcePathAddress.address.toInt(),
385         old_pathSize = sourcePathSize,
386         new_fd = preopenForPath(target)?.fd ?: throw FileNotFoundException("no preopen: $target"),
387         new_path = targetPathAddress.address.toInt(),
388         new_pathSize = targetPathSize,
389       )
390       if (errno == Errno.noent.ordinal) {
391         throw FileNotFoundException("no such file: $source")
392       }
393 
394       if (errno != 0) throw ErrnoException(errno.toShort())
395     }
396   }
397 
398   override fun delete(path: Path, mustExist: Boolean) {
399     withScopedMemoryAllocator { allocator ->
400       val (pathAddress, pathSize) = allocator.write(path.toString())
401       val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path")
402 
403       var errno = path_unlink_file(
404         fd = preopenFd.fd,
405         path = pathAddress.address.toInt(),
406         pathSize = pathSize,
407       )
408       // If unlink failed, try remove_directory.
409       when (errno) {
410         Errno.noent.ordinal -> {
411           if (mustExist) throw FileNotFoundException("no such file: $path")
412           return // Nothing to delete.
413         }
414 
415         Errno.perm.ordinal,
416         Errno.isdir.ordinal,
417         -> {
418           errno = path_remove_directory(
419             fd = preopenFd.fd,
420             path = pathAddress.address.toInt(),
421             pathSize = pathSize,
422           )
423         }
424       }
425       if (errno != 0) throw ErrnoException(errno.toShort())
426     }
427   }
428 
429   override fun createSymlink(source: Path, target: Path) {
430     withScopedMemoryAllocator { allocator ->
431       val sourcePreopen = preopenForPath(source)
432         ?: throw FileNotFoundException("no preopen: $source")
433 
434       // Always create symlinks relative to their source. Absolute symlinks are trouble because the
435       // absolute paths used by WASI are different from the absolute paths on the host file system.
436       val sourceParent = source.parent
437         ?: throw IOException("unexpected symlink source: $source")
438       val targetRelative = when {
439         target.isRelative -> target
440         else -> target.relativeTo(sourceParent)
441       }
442 
443       val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString())
444       val (targetPathAddress, targetPathSize) = allocator.write(targetRelative.toString())
445 
446       val errno = path_symlink(
447         old_path = targetPathAddress.address.toInt(),
448         old_pathSize = targetPathSize,
449         fd = sourcePreopen.fd,
450         new_path = sourcePathAddress.address.toInt(),
451         new_pathSize = sourcePathSize,
452       )
453       if (errno != 0) throw ErrnoException(errno.toShort())
454     }
455   }
456 
457   private fun pathOpen(
458     path: Path,
459     oflags: oflags,
460     rightsBase: rights,
461     fdflags: fdflags = 0,
462   ): fd {
463     withScopedMemoryAllocator { allocator ->
464       val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path")
465       val (pathAddress, pathSize) = allocator.write(path.toString())
466 
467       val returnPointer: Pointer = allocator.allocate(4) // fd is u32.
468       val errno = path_open(
469         fd = preopenFd.fd,
470         dirflags = 0,
471         path = pathAddress.address.toInt(),
472         pathSize = pathSize,
473         oflags = oflags,
474         fs_rights_base = rightsBase,
475         fs_rights_inheriting = 0,
476         fdflags = fdflags,
477         returnPointer = returnPointer.address.toInt(),
478       )
479       if (errno == Errno.noent.ordinal) {
480         throw FileNotFoundException("no such file: $path")
481       }
482       if (errno != 0) throw ErrnoException(errno.toShort())
483       return returnPointer.loadInt()
484     }
485   }
486 
487   /**
488    * Returns the preopen whose path is either an ancestor of [path], or whose path [path] is an
489    * ancestor of.
490    *
491    * If [path] is an ancestor of our preopen, then operating on the path will ultimately fail with a
492    * `notcapable` errno.
493    */
494   private fun preopenForPath(path: Path): Preopen? {
495     if (path.isRelative) return relativePathPreopen
496 
497     val pathSegmentsBytes = path.segmentsBytes
498 
499     return preopens.firstOrNull { preopen ->
500       val commonSize = minOf(pathSegmentsBytes.size, preopen.segmentsBytes.size)
501       preopen.segmentsBytes.subList(0, commonSize) == pathSegmentsBytes.subList(0, commonSize)
502     }
503   }
504 
505   override fun toString() = "okio.WasiFileSystem"
506 
507   private class Preopen(
508     val path: Path,
509     val segmentsBytes: List<ByteString>,
510     val fd: fd,
511   )
512 }
513