package sysimport (socketapi)const (FdStdinint32 = iotaFdStdoutFdStderr// FdPreopen is the file descriptor of the first pre-opened directory. // // # Why file descriptor 3? // // While not specified, the most common WASI implementation, wasi-libc, // expects POSIX style file descriptor allocation, where the lowest // available number is used to open the next file. Since 1 and 2 are taken // by stdout and stderr, the next is 3. // - https://github.com/WebAssembly/WASI/issues/122 // - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14 // - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215FdPreopen)const modeDevice = fs.ModeDevice | 0o640// FileEntry maps a path to an open file in a file system.typeFileEntrystruct {// Name is the name of the directory up to its pre-open, or the pre-open // name itself when IsPreopen. // // # Notes // // - This can drift on rename. // - This relates to the guest path, which is not the real file path // except if the entire host filesystem was made available. Name string// IsPreopen is a directory that is lazily opened. IsPreopen bool// FS is the filesystem associated with the pre-open. FS sys.FS// File is always non-nil. File fsapi.File// direntCache is nil until DirentCache was called. direntCache *DirentCache}// DirentCache gets or creates a DirentCache for this file or returns an error.//// # Errors//// A zero sys.Errno is success. The below are expected otherwise:// - sys.ENOSYS: the implementation does not support this function.// - sys.EBADF: the dir was closed or not readable.// - sys.ENOTDIR: the file was not a directory.//// # Notes//// - See /RATIONALE.md for design notes.func ( *FileEntry) () (*DirentCache, sys.Errno) {if := .direntCache; != nil {return , 0 }// Require the file to be a directory vs a late error on the same.if , := .File.IsDir(); != 0 {returnnil, } elseif ! {returnnil, sys.ENOTDIR }// Generate the dotEntries only once.if , := synthesizeDotEntries(); != 0 {returnnil, } else { .direntCache = &DirentCache{f: .File, dotEntries: } }return .direntCache, 0}// DirentCache is a caching abstraction of sys.File Readdir.//// This is special-cased for "wasi_snapshot_preview1.fd_readdir", and may be// unneeded, or require changes, to support preview1 or preview2.// - The position of the dirents are serialized as `d_next`. For reasons// described below, any may need to be re-read. This accepts any positions// in the cache, rather than track the position of the last dirent.// - dot entries ("." and "..") must be returned. See /RATIONALE.md for why.// - An sys.Dirent Name is variable length, it could exceed memory size and// need to be re-read.// - Multiple dirents may be returned. It is more efficient to read from the// underlying file in bulk vs one-at-a-time.//// The last results returned by Read are cached, but entries before that// position are not. This support re-reading entries that couldn't fit into// memory without accidentally caching all entries in a large directory. This// approach is sometimes called a sliding window.typeDirentCachestruct {// f is the underlying file f sys.File// dotEntries are the "." and ".." entries added when the directory is // initialized. dotEntries []sys.Dirent// dirents are the potentially unread directory entries. // // Internal detail: nil is different from zero length. Zero length is an // exhausted directory (eof). nil means the re-read. dirents []sys.Dirent// countRead is the total count of dirents read since last rewind. countRead uint64// eof is true when the underlying file is at EOF. This avoids re-reading // the directory when it is exhausted. Entires in an exhausted directory // are not visible until it is rewound via calling Read with `pos==0`. eof bool}// synthesizeDotEntries generates a slice of the two elements "." and "..".func synthesizeDotEntries( *FileEntry) ([]sys.Dirent, sys.Errno) { , := .File.Ino()if != 0 {returnnil, } := [2]sys.Dirent{} [0] = sys.Dirent{Name: ".", Ino: , Type: fs.ModeDir}// See /RATIONALE.md for why we don't attempt to get an inode for ".." and // why in wasi-libc this won't fan-out either. [1] = sys.Dirent{Name: "..", Ino: 0, Type: fs.ModeDir}return [:], 0}// exhaustedDirents avoids allocating empty slices.var exhaustedDirents = [0]sys.Dirent{}// Read is similar to and returns the same errors as `Readdir` on sys.File.// The main difference is this caches entries returned, resulting in multiple// valid positions to read from.//// When zero, `pos` means rewind to the beginning of this directory. This// implies a rewind (Seek to zero on the underlying sys.File), unless the// initial entries are still cached.//// When non-zero, `pos` is the zero based index of all dirents returned since// last rewind. Only entries beginning at `pos` are cached for subsequent// calls. A non-zero `pos` before the cache returns sys.ENOENT for reasons// described on DirentCache documentation.//// Up to `n` entries are cached and returned. When `n` exceeds the cache, the// difference are read from the underlying sys.File via `Readdir`. EOF is// when `len(dirents)` returned are less than `n`.func ( *DirentCache) ( uint64, uint32) ( []sys.Dirent, sys.Errno) {switch {case > .countRead: // farther than read or negative coerced to uint64.returnnil, sys.ENOENTcase == 0 && .dirents != nil:// Rewind if we have already read entries. This allows us to see new // entries added after the directory was opened.if _, = .f.Seek(0, io.SeekStart); != 0 {return } .dirents = nil// dump cache .countRead = 0 }if == 0 {return// special case no entries. }if .dirents == nil {// Always populate dot entries, which makes min len(dirents) == 2. .dirents = .dotEntries .countRead = 2 .eof = falseif := int( - 2); <= 0 {return } elseif , = .f.Readdir(); != 0 {return } elseif := len(); > 0 { .eof = < .dirents = append(.dotEntries, ...) .countRead += uint64() }return .cachedDirents(), 0 }// Reset our cache to the first entry being read. := .countRead - uint64(len(.dirents))if < {// We don't currently allow reads before our cache because Seek(0) is // the only portable way. Doing otherwise requires skipping, which we // won't do unless wasi-testsuite starts requiring it. Implementing // this would allow re-reading a large directory, so care would be // needed to not buffer the entire directory in memory while skipping. = sys.ENOENTreturn } elseif := - ; != 0 {ifuint64(len(.dirents)) == {// Avoid allocation re-slicing to zero length. .dirents = exhaustedDirents[:] } else { .dirents = .dirents[:] } }// See if we need more entries.if := int() - len(.dirents); > 0 && !.eof {// Try to read more, which could fail.if , = .f.Readdir(); != 0 {return }// Append the next read entries if we weren't at EOF.if := len(); > 0 { .eof = < .dirents = append(.dirents, ...) .countRead += uint64() } }return .cachedDirents(), 0}// cachedDirents returns up to `n` dirents from the cache.func ( *DirentCache) ( uint32) []sys.Dirent { := uint32(len(.dirents))switch {case == 0:returnnilcase > :return .dirents[:] }return .dirents}typeFSContextstruct {// openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files // (or directories) and defaults to empty. // TODO: This is unguarded, so not goroutine-safe! openedFiles FileTable}// FileTable is a specialization of the descriptor.Table type used to map file// descriptors to file entries.typeFileTable = descriptor.Table[int32, *FileEntry]// LookupFile returns a file if it is in the table.func ( *FSContext) ( int32) (*FileEntry, bool) {return .openedFiles.Lookup()}// OpenFile opens the file into the table and returns its file descriptor.// The result must be closed by CloseFile or Close.func ( *FSContext) ( sys.FS, string, sys.Oflag, fs.FileMode) (int32, sys.Errno) {if , := .OpenFile(, , ); != 0 {return0, } else { := &FileEntry{FS: , File: fsapi.Adapt()}if == "/" || == "." { .Name = "" } else { .Name = }if , := .openedFiles.Insert(); ! {return0, sys.EBADF } else {return , 0 } }}// Renumber assigns the file pointed by the descriptor `from` to `to`.func ( *FSContext) (, int32) sys.Errno { , := .openedFiles.Lookup()if ! || < 0 {returnsys.EBADF } elseif .IsPreopen {returnsys.ENOTSUP }// If toFile is already open, we close it to prevent windows lock issues. // // The doc is unclear and other implementations do nothing for already-opened To FDs. // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno // https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546if , := .openedFiles.Lookup(); {if .IsPreopen {returnsys.ENOTSUP } _ = .File.Close() } .openedFiles.Delete()if !.openedFiles.InsertAt(, ) {returnsys.EBADF }return0}// SockAccept accepts a sock.TCPConn into the file table and returns its file// descriptor.func ( *FSContext) ( int32, bool) (int32, sys.Errno) {varsocketapi.TCPSockif , := .LookupFile(); ! || !.IsPreopen {return0, sys.EBADF// Not a preopen } elseif , = .File.(socketapi.TCPSock); ! {return0, sys.EBADF// Not a sock } , := .Accept()if != 0 {return0, } := &FileEntry{File: fsapi.Adapt()}if {if = .File.SetNonblock(true); != 0 { _ = .Close()return0, } }if , := .openedFiles.Insert(); ! {return0, sys.EBADF } else {return , 0 }}// CloseFile returns any error closing the existing file.func ( *FSContext) ( int32) ( sys.Errno) { , := .openedFiles.Lookup()if ! {returnsys.EBADF }if = .File.Close(); != 0 {return } .openedFiles.Delete()return}// Close implements io.Closerfunc ( *FSContext) () ( error) {// Close any files opened in this context .openedFiles.Range(func( int32, *FileEntry) bool {if := .File.Close(); != 0 { = // This means err returned == the last non-nil error. }returntrue })// A closed FSContext cannot be reused so clear the state. .openedFiles = FileTable{}return}// InitFSContext initializes a FSContext with stdio streams and optional// pre-opened filesystems and TCP listeners.func ( *Context) (io.Reader, , io.Writer, []sys.FS, []string, []*net.TCPListener,) ( error) { , := stdinFileEntry()if != nil {return } .fsc.openedFiles.Insert() , := stdioWriterFileEntry("stdout", )if != nil {return } .fsc.openedFiles.Insert() , := stdioWriterFileEntry("stderr", )if != nil {return } .fsc.openedFiles.Insert()for , := range { := []ifStripPrefixesAndTrailingSlash() == "" {// Default to bind to '/' when guestPath is effectively empty. = "/" } .fsc.openedFiles.Insert(&FileEntry{FS: ,Name: ,IsPreopen: true,File: &lazyDir{fs: }, }) }for , := range { .fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: fsapi.Adapt(sysfs.NewTCPListenerFile())}) }returnnil}// StripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the// result index begins with another string. A result of "." coerces to the// empty string "" because the current directory is handled by the guest.//// Results are the offset/len pair which is an optimization to avoid re-slicing// overhead, as this function is called for every path operation.//// Note: Relative paths should be handled by the guest, as that's what knows// what the current directory is. However, paths that escape the current// directory e.g. "../.." have been found in `tinygo test` and this// implementation takes care to avoid it.func ( string) string {// strip trailing slashes := len()for ; > 0 && [-1] == '/'; -- { } := 0:for < {switch [] {case'/': ++case'.': := + 1if < && [] == '/' { = + 1 } elseif == { = } else {break }default:break } }return [:]}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.