Buckets:
| // Copyright 2021 The Emscripten Authors. All rights reserved. | |
| // Emscripten is available under two separate licenses, the MIT license and the | |
| // University of Illinois/NCSA Open Source License. Both these licenses can be | |
| // found in the LICENSE file. | |
| // Syscall implementations. | |
| // File permission macros for wasmfs. | |
| // Used to improve readability compared to those in stat.h | |
| // In Linux, the maximum length for a filename is 255 bytes. | |
| extern "C" { | |
| using namespace wasmfs; | |
| int __syscall_dup3(int oldfd, int newfd, int flags) { | |
| if (flags & ~O_CLOEXEC) { | |
| return -EINVAL; | |
| } | |
| if (oldfd == newfd) { | |
| return -EINVAL; | |
| } | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| auto oldOpenFile = fileTable.getEntry(oldfd); | |
| if (!oldOpenFile) { | |
| return -EBADF; | |
| } | |
| if (newfd < 0 || newfd >= WASMFS_FD_MAX) { | |
| return -EBADF; | |
| } | |
| // If the file descriptor newfd was previously open, it will just be | |
| // overwritten silently. | |
| (void)fileTable.setEntry(newfd, oldOpenFile); | |
| return newfd; | |
| } | |
| int __syscall_dup(int fd) { | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| // Check that an open file exists corresponding to the given fd. | |
| auto openFile = fileTable.getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| return fileTable.addEntry(openFile); | |
| } | |
| // This enum specifies whether file offset will be provided by the open file | |
| // state or provided by argument in the case of pread or pwrite. | |
| enum class OffsetHandling { OpenFileState, Argument }; | |
| // Internal write function called by __wasi_fd_write and __wasi_fd_pwrite | |
| // Receives an open file state offset. | |
| // Optionally sets open file state offset. | |
| static __wasi_errno_t writeAtOffset(OffsetHandling setOffset, | |
| __wasi_fd_t fd, | |
| const __wasi_ciovec_t* iovs, | |
| size_t iovs_len, | |
| __wasi_size_t* nwritten, | |
| __wasi_filesize_t offset = 0) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return __WASI_ERRNO_BADF; | |
| } | |
| if (iovs_len < 0 || offset < 0) { | |
| return __WASI_ERRNO_INVAL; | |
| } | |
| auto lockedOpenFile = openFile->locked(); | |
| auto file = lockedOpenFile.getFile()->dynCast<DataFile>(); | |
| if (!file) { | |
| return __WASI_ERRNO_ISDIR; | |
| } | |
| auto lockedFile = file->locked(); | |
| if (setOffset == OffsetHandling::OpenFileState) { | |
| if (lockedOpenFile.getFlags() & O_APPEND) { | |
| off_t size = lockedFile.getSize(); | |
| if (size < 0) { | |
| // Translate to WASI standard of positive return codes. | |
| return -size; | |
| } | |
| offset = size; | |
| lockedOpenFile.setPosition(offset); | |
| } else { | |
| offset = lockedOpenFile.getPosition(); | |
| } | |
| } | |
| // TODO: Check open file access mode for write permissions. | |
| size_t bytesWritten = 0; | |
| for (size_t i = 0; i < iovs_len; i++) { | |
| const uint8_t* buf = iovs[i].buf; | |
| off_t len = iovs[i].buf_len; | |
| // Check if buf_len specifies a positive length buffer but buf is a | |
| // null pointer | |
| if (!buf && len > 0) { | |
| return __WASI_ERRNO_INVAL; | |
| } | |
| // Check if the sum of the buf_len values overflows an off_t (63 bits). | |
| if (addWillOverFlow(offset, (__wasi_filesize_t)bytesWritten)) { | |
| return __WASI_ERRNO_FBIG; | |
| } | |
| auto result = lockedFile.write(buf, len, offset + bytesWritten); | |
| if (result < 0) { | |
| // This individual write failed. Report the error unless we've already | |
| // written some bytes, in which case report a successful short write. | |
| if (bytesWritten > 0) { | |
| break; | |
| } | |
| return -result; | |
| } | |
| // The write was successful. | |
| bytesWritten += result; | |
| if (result < len) { | |
| // The write was short, so stop here. | |
| break; | |
| } | |
| } | |
| *nwritten = bytesWritten; | |
| if (setOffset == OffsetHandling::OpenFileState && | |
| lockedOpenFile.getFile()->isSeekable()) { | |
| lockedOpenFile.setPosition(offset + bytesWritten); | |
| } | |
| if (bytesWritten) { | |
| lockedFile.updateMTime(); | |
| } | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| // Internal read function called by __wasi_fd_read and __wasi_fd_pread | |
| // Receives an open file state offset. | |
| // Optionally sets open file state offset. | |
| // TODO: combine this with writeAtOffset because the code is nearly identical. | |
| static __wasi_errno_t readAtOffset(OffsetHandling setOffset, | |
| __wasi_fd_t fd, | |
| const __wasi_iovec_t* iovs, | |
| size_t iovs_len, | |
| __wasi_size_t* nread, | |
| __wasi_filesize_t offset = 0) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return __WASI_ERRNO_BADF; | |
| } | |
| auto lockedOpenFile = openFile->locked(); | |
| if (setOffset == OffsetHandling::OpenFileState) { | |
| offset = lockedOpenFile.getPosition(); | |
| } | |
| if (iovs_len < 0 || offset < 0) { | |
| return __WASI_ERRNO_INVAL; | |
| } | |
| // TODO: Check open file access mode for read permissions. | |
| auto file = lockedOpenFile.getFile()->dynCast<DataFile>(); | |
| // If file is nullptr, then the file was not a DataFile. | |
| if (!file) { | |
| return __WASI_ERRNO_ISDIR; | |
| } | |
| auto lockedFile = file->locked(); | |
| size_t bytesRead = 0; | |
| for (size_t i = 0; i < iovs_len; i++) { | |
| uint8_t* buf = iovs[i].buf; | |
| size_t len = iovs[i].buf_len; | |
| if (!buf && len > 0) { | |
| return __WASI_ERRNO_INVAL; | |
| } | |
| // TODO: Check for overflow when adding offset + bytesRead. | |
| auto result = lockedFile.read(buf, len, offset + bytesRead); | |
| if (result < 0) { | |
| // This individual read failed. Report the error unless we've already read | |
| // some bytes, in which case report a successful short read. | |
| if (bytesRead > 0) { | |
| break; | |
| } | |
| return -result; | |
| } | |
| // The read was successful. | |
| // Backends must only return len or less. | |
| assert(result <= len); | |
| bytesRead += result; | |
| if (result < len) { | |
| // The read was short, so stop here. | |
| break; | |
| } | |
| } | |
| *nread = bytesRead; | |
| if (setOffset == OffsetHandling::OpenFileState && | |
| lockedOpenFile.getFile()->isSeekable()) { | |
| lockedOpenFile.setPosition(offset + bytesRead); | |
| } | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| __wasi_errno_t __wasi_fd_write(__wasi_fd_t fd, | |
| const __wasi_ciovec_t* iovs, | |
| size_t iovs_len, | |
| __wasi_size_t* nwritten) { | |
| return writeAtOffset( | |
| OffsetHandling::OpenFileState, fd, iovs, iovs_len, nwritten); | |
| } | |
| __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, | |
| const __wasi_iovec_t* iovs, | |
| size_t iovs_len, | |
| __wasi_size_t* nread) { | |
| return readAtOffset(OffsetHandling::OpenFileState, fd, iovs, iovs_len, nread); | |
| } | |
| __wasi_errno_t __wasi_fd_pwrite(__wasi_fd_t fd, | |
| const __wasi_ciovec_t* iovs, | |
| size_t iovs_len, | |
| __wasi_filesize_t offset, | |
| __wasi_size_t* nwritten) { | |
| return writeAtOffset( | |
| OffsetHandling::Argument, fd, iovs, iovs_len, nwritten, offset); | |
| } | |
| __wasi_errno_t __wasi_fd_pread(__wasi_fd_t fd, | |
| const __wasi_iovec_t* iovs, | |
| size_t iovs_len, | |
| __wasi_filesize_t offset, | |
| __wasi_size_t* nread) { | |
| return readAtOffset( | |
| OffsetHandling::Argument, fd, iovs, iovs_len, nread, offset); | |
| } | |
| __wasi_errno_t __wasi_fd_close(__wasi_fd_t fd) { | |
| std::shared_ptr<DataFile> closee; | |
| { | |
| // Do not hold the file table lock while performing the close. | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| auto entry = fileTable.getEntry(fd); | |
| if (!entry) { | |
| return __WASI_ERRNO_BADF; | |
| } | |
| closee = fileTable.setEntry(fd, nullptr); | |
| } | |
| if (closee) { | |
| // Translate to WASI standard of positive return codes. | |
| int ret = -closee->locked().close(); | |
| assert(ret >= 0); | |
| return ret; | |
| } | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| __wasi_errno_t __wasi_fd_sync(__wasi_fd_t fd) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return __WASI_ERRNO_BADF; | |
| } | |
| // Nothing to flush for anything but a data file, but also not an error either | |
| // way. TODO: in the future we may want syncing of directories. | |
| auto dataFile = openFile->locked().getFile()->dynCast<DataFile>(); | |
| if (dataFile) { | |
| auto ret = dataFile->locked().flush(); | |
| assert(ret <= 0); | |
| // Translate to WASI standard of positive return codes. | |
| return -ret; | |
| } | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| int __syscall_fdatasync(int fd) { | |
| // TODO: Optimize this to avoid unnecessarily flushing unnecessary metadata. | |
| return __wasi_fd_sync(fd); | |
| } | |
| backend_t wasmfs_get_backend_by_fd(int fd) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return NullBackend; | |
| } | |
| return openFile->locked().getFile()->getBackend(); | |
| } | |
| // This function is exposed to users to allow them to obtain a backend_t for a | |
| // specified path. | |
| backend_t wasmfs_get_backend_by_path(const char* path) { | |
| auto parsed = path::parseFile(path); | |
| if (parsed.getError()) { | |
| // Could not find the file. | |
| return NullBackend; | |
| } | |
| return parsed.getFile()->getBackend(); | |
| } | |
| static timespec ms_to_timespec(double ms) { | |
| long long seconds = ms / 1000; | |
| timespec ts; | |
| ts.tv_sec = seconds; // seconds | |
| ts.tv_nsec = (ms - (seconds * 1000)) * 1000 * 1000; // nanoseconds | |
| return ts; | |
| } | |
| int __syscall_newfstatat(int dirfd, intptr_t path, intptr_t buf, int flags) { | |
| // Only accept valid flags. | |
| if (flags & ~(AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW)) { | |
| // TODO: Test this case. | |
| return -EINVAL; | |
| } | |
| auto parsed = path::getFileAt(dirfd, (char*)path, flags); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto file = parsed.getFile(); | |
| // Extract the information from the file. | |
| auto lockedFile = file->locked(); | |
| auto buffer = (struct stat*)buf; | |
| off_t size = lockedFile.getSize(); | |
| if (size < 0) { | |
| return size; | |
| } | |
| buffer->st_size = size; | |
| // ATTN: hard-coded constant values are copied from the existing JS file | |
| // system. Specific values were chosen to match existing library_fs.js | |
| // values. | |
| // ID of device containing file: Hardcode 1 for now, no meaning at the | |
| // moment for Emscripten. | |
| buffer->st_dev = 1; | |
| buffer->st_mode = lockedFile.getMode(); | |
| buffer->st_ino = file->getIno(); | |
| // The number of hard links is 1 since they are unsupported. | |
| buffer->st_nlink = 1; | |
| buffer->st_uid = 0; | |
| buffer->st_gid = 0; | |
| // Device ID (if special file) No meaning right now for Emscripten. | |
| buffer->st_rdev = 0; | |
| // The syscall docs state this is hardcoded to # of 512 byte blocks. | |
| buffer->st_blocks = (buffer->st_size + 511) / 512; | |
| // Specifies the preferred blocksize for efficient disk I/O. | |
| buffer->st_blksize = 4096; | |
| buffer->st_atim = ms_to_timespec(lockedFile.getATime()); | |
| buffer->st_mtim = ms_to_timespec(lockedFile.getMTime()); | |
| buffer->st_ctim = ms_to_timespec(lockedFile.getCTime()); | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| int __syscall_stat64(intptr_t path, intptr_t buf) { | |
| return __syscall_newfstatat(AT_FDCWD, path, buf, 0); | |
| } | |
| int __syscall_lstat64(intptr_t path, intptr_t buf) { | |
| return __syscall_newfstatat(AT_FDCWD, path, buf, AT_SYMLINK_NOFOLLOW); | |
| } | |
| int __syscall_fstat64(int fd, intptr_t buf) { | |
| return __syscall_newfstatat(fd, (intptr_t) "", buf, AT_EMPTY_PATH); | |
| } | |
| // When calling doOpen(), we may request an FD be returned, or we may not need | |
| // that return value (in which case no FD need be allocated, and we return 0 on | |
| // success). | |
| enum class OpenReturnMode { FD, Nothing }; | |
| static __wasi_fd_t doOpen(path::ParsedParent parsed, | |
| int flags, | |
| mode_t mode, | |
| backend_t backend = NullBackend, | |
| OpenReturnMode returnMode = OpenReturnMode::FD) { | |
| int accessMode = (flags & O_ACCMODE); | |
| if (accessMode != O_WRONLY && accessMode != O_RDONLY && | |
| accessMode != O_RDWR) { | |
| return -EINVAL; | |
| } | |
| // TODO: remove assert when all functionality is complete. | |
| assert((flags & ~(O_CREAT | O_EXCL | O_DIRECTORY | O_TRUNC | O_APPEND | | |
| O_RDWR | O_WRONLY | O_RDONLY | O_LARGEFILE | O_NOFOLLOW | | |
| O_CLOEXEC | O_NONBLOCK)) == 0); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto& [parent, childName] = parsed.getParentChild(); | |
| if (childName.size() > WASMFS_NAME_MAX) { | |
| return -ENAMETOOLONG; | |
| } | |
| std::shared_ptr<File> child; | |
| { | |
| auto lockedParent = parent->locked(); | |
| child = lockedParent.getChild(std::string(childName)); | |
| // The requested node was not found. | |
| if (!child) { | |
| // If curr is the last element and the create flag is specified | |
| // If O_DIRECTORY is also specified, still create a regular file: | |
| // https://man7.org/linux/man-pages/man2/open.2.html#BUGS | |
| if (!(flags & O_CREAT)) { | |
| return -ENOENT; | |
| } | |
| // Inserting into an unlinked directory is not allowed. | |
| if (!lockedParent.getParent()) { | |
| return -ENOENT; | |
| } | |
| // Mask out everything except the permissions bits. | |
| mode &= S_IALLUGO; | |
| mode &= ~wasmFS.getUmask(); | |
| // If there is no explicitly provided backend, use the parent's backend. | |
| if (!backend) { | |
| backend = parent->getBackend(); | |
| } | |
| // TODO: Check write permissions on the parent directory. | |
| std::shared_ptr<File> created; | |
| if (backend == parent->getBackend()) { | |
| created = lockedParent.insertDataFile(std::string(childName), mode); | |
| if (!created) { | |
| // TODO Receive a specific error code, and report it here. For now, | |
| // report a generic error. | |
| return -EIO; | |
| } | |
| } else { | |
| created = backend->createFile(mode); | |
| if (!created) { | |
| // TODO Receive a specific error code, and report it here. For now, | |
| // report a generic error. | |
| return -EIO; | |
| } | |
| [[maybe_unused]] bool mounted = | |
| lockedParent.mountChild(std::string(childName), created); | |
| assert(mounted); | |
| } | |
| // TODO: Check that the insert actually succeeds. | |
| if (returnMode == OpenReturnMode::Nothing) { | |
| return 0; | |
| } | |
| std::shared_ptr<OpenFileState> openFile; | |
| if (auto err = OpenFileState::create(created, flags, openFile)) { | |
| assert(err < 0); | |
| return err; | |
| } | |
| return wasmFS.getFileTable().locked().addEntry(openFile); | |
| } | |
| } | |
| if (auto link = child->dynCast<Symlink>()) { | |
| if (flags & O_NOFOLLOW) { | |
| return -ELOOP; | |
| } | |
| // TODO: The link dereference count starts back at 0 here. We could | |
| // propagate it from the previous path parsing instead. | |
| auto target = link->getTarget(); | |
| auto parsedLink = path::getFileFrom(parent, target); | |
| if (auto err = parsedLink.getError()) { | |
| return err; | |
| } | |
| child = parsedLink.getFile(); | |
| } | |
| assert(!child->is<Symlink>()); | |
| // Return an error if the file exists and O_CREAT and O_EXCL are specified. | |
| if ((flags & O_EXCL) && (flags & O_CREAT)) { | |
| return -EEXIST; | |
| } | |
| if (child->is<Directory>() && (accessMode != O_RDONLY || (flags & O_CREAT))) { | |
| return -EISDIR; | |
| } | |
| // Check user permissions. | |
| auto fileMode = child->locked().getMode(); | |
| if ((accessMode == O_RDONLY || accessMode == O_RDWR) && | |
| !(fileMode & WASMFS_PERM_READ)) { | |
| return -EACCES; | |
| } | |
| if ((accessMode == O_WRONLY || accessMode == O_RDWR) && | |
| !(fileMode & WASMFS_PERM_WRITE)) { | |
| return -EACCES; | |
| } | |
| // Fail if O_DIRECTORY is specified and pathname is not a directory | |
| if (flags & O_DIRECTORY && !child->is<Directory>()) { | |
| return -ENOTDIR; | |
| } | |
| // Note that we open the file before truncating it because some backends may | |
| // truncate opened files more efficiently (e.g. OPFS). | |
| std::shared_ptr<OpenFileState> openFile; | |
| if (auto err = OpenFileState::create(child, flags, openFile)) { | |
| assert(err < 0); | |
| return err; | |
| } | |
| // If O_TRUNC, truncate the file if possible. | |
| if (flags & O_TRUNC) { | |
| if (!child->is<DataFile>()) { | |
| return -EISDIR; | |
| } | |
| if ((fileMode & WASMFS_PERM_WRITE) == 0) { | |
| return -EACCES; | |
| } | |
| // Try to truncate the file, continuing silently if we cannot. | |
| (void)child->cast<DataFile>()->locked().setSize(0); | |
| } | |
| return wasmFS.getFileTable().locked().addEntry(openFile); | |
| } | |
| // This function is exposed to users and allows users to create a file in a | |
| // specific backend. An fd to an open file is returned. | |
| int wasmfs_create_file(char* pathname, mode_t mode, backend_t backend) { | |
| static_assert(std::is_same_v<decltype(doOpen(0, 0, 0, 0)), unsigned int>, | |
| "unexpected conversion from result of doOpen to int"); | |
| return doOpen( | |
| path::parseParent((char*)pathname), O_CREAT | O_EXCL, mode, backend); | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_openat(int dirfd, intptr_t path, int flags, ...) { | |
| mode_t mode = 0; | |
| va_list v1; | |
| va_start(v1, flags); | |
| mode = va_arg(v1, int); | |
| va_end(v1); | |
| return doOpen(path::parseParent((char*)path, dirfd), flags, mode); | |
| } | |
| int __syscall_mknodat(int dirfd, intptr_t path, int mode, int dev) { | |
| assert(dev == 0); // TODO: support special devices | |
| if (mode & S_IFDIR) { | |
| return -EINVAL; | |
| } | |
| if (mode & S_IFIFO) { | |
| return -EPERM; | |
| } | |
| return doOpen(path::parseParent((char*)path, dirfd), | |
| O_CREAT | O_EXCL, | |
| mode, | |
| NullBackend, | |
| OpenReturnMode::Nothing); | |
| } | |
| static int | |
| doMkdir(path::ParsedParent parsed, int mode, backend_t backend = NullBackend) { | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto& [parent, childNameView] = parsed.getParentChild(); | |
| std::string childName(childNameView); | |
| auto lockedParent = parent->locked(); | |
| if (childName.size() > WASMFS_NAME_MAX) { | |
| return -ENAMETOOLONG; | |
| } | |
| // Check if the requested directory already exists. | |
| if (lockedParent.getChild(childName)) { | |
| return -EEXIST; | |
| } | |
| // Mask rwx permissions for user, group and others, and the sticky bit. | |
| // This prevents users from entering S_IFREG for example. | |
| // https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html | |
| mode &= S_IRWXUGO | S_ISVTX; | |
| mode &= ~wasmFS.getUmask(); | |
| if (!(lockedParent.getMode() & WASMFS_PERM_WRITE)) { | |
| return -EACCES; | |
| } | |
| // By default, the backend that the directory is created in is the same as | |
| // the parent directory. However, if a backend is passed as a parameter, | |
| // then that backend is used. | |
| if (!backend) { | |
| backend = parent->getBackend(); | |
| } | |
| if (backend == parent->getBackend()) { | |
| if (!lockedParent.insertDirectory(childName, mode)) { | |
| // TODO Receive a specific error code, and report it here. For now, report | |
| // a generic error. | |
| return -EIO; | |
| } | |
| } else { | |
| auto created = backend->createDirectory(mode); | |
| if (!created) { | |
| // TODO Receive a specific error code, and report it here. For now, report | |
| // a generic error. | |
| return -EIO; | |
| } | |
| [[maybe_unused]] bool mounted = lockedParent.mountChild(childName, created); | |
| assert(mounted); | |
| } | |
| // TODO: Check that the insertion is successful. | |
| return 0; | |
| } | |
| // This function is exposed to users and allows users to specify a particular | |
| // backend that a directory should be created within. | |
| int wasmfs_create_directory(char* path, int mode, backend_t backend) { | |
| static_assert(std::is_same_v<decltype(doMkdir(0, 0, 0)), int>, | |
| "unexpected conversion from result of doMkdir to int"); | |
| return doMkdir(path::parseParent(path), mode, backend); | |
| } | |
| // TODO: Test this. | |
| int __syscall_mkdirat(int dirfd, intptr_t path, int mode) { | |
| return doMkdir(path::parseParent((char*)path, dirfd), mode); | |
| } | |
| int __syscall_umask(int mask) { | |
| mode_t old = wasmFS.getUmask(); | |
| wasmFS.setUmask(mask); | |
| return old; | |
| } | |
| __wasi_errno_t __wasi_fd_seek(__wasi_fd_t fd, | |
| __wasi_filedelta_t offset, | |
| __wasi_whence_t whence, | |
| __wasi_filesize_t* newoffset) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return __WASI_ERRNO_BADF; | |
| } | |
| auto lockedOpenFile = openFile->locked(); | |
| if (!lockedOpenFile.getFile()->isSeekable()) { | |
| return __WASI_ERRNO_SPIPE; | |
| } | |
| off_t position; | |
| if (whence == SEEK_SET) { | |
| position = offset; | |
| } else if (whence == SEEK_CUR) { | |
| position = lockedOpenFile.getPosition() + offset; | |
| } else if (whence == SEEK_END) { | |
| // Only the open file state is altered in seek. Locking the underlying | |
| // data file here once is sufficient. | |
| off_t size = lockedOpenFile.getFile()->locked().getSize(); | |
| if (size < 0) { | |
| // Translate to WASI standard of positive return codes. | |
| return -size; | |
| } | |
| position = size + offset; | |
| } else { | |
| return __WASI_ERRNO_INVAL; | |
| } | |
| if (position < 0) { | |
| return __WASI_ERRNO_INVAL; | |
| } | |
| lockedOpenFile.setPosition(position); | |
| if (newoffset) { | |
| *newoffset = position; | |
| } | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| static int doChdir(std::shared_ptr<File>& file) { | |
| auto dir = file->dynCast<Directory>(); | |
| if (!dir) { | |
| return -ENOTDIR; | |
| } | |
| wasmFS.setCWD(dir); | |
| return 0; | |
| } | |
| int __syscall_chdir(intptr_t path) { | |
| auto parsed = path::parseFile((char*)path); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| return doChdir(parsed.getFile()); | |
| } | |
| int __syscall_fchdir(int fd) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| return doChdir(openFile->locked().getFile()); | |
| } | |
| int __syscall_getcwd(intptr_t buf, size_t size) { | |
| // Check if buf points to a bad address. | |
| if (!buf && size > 0) { | |
| return -EFAULT; | |
| } | |
| // Check if the size argument is zero and buf is not a null pointer. | |
| if (buf && size == 0) { | |
| return -EINVAL; | |
| } | |
| auto curr = wasmFS.getCWD(); | |
| std::string result = ""; | |
| while (curr != wasmFS.getRootDirectory()) { | |
| auto parent = curr->locked().getParent(); | |
| // Check if the parent exists. The parent may not exist if the CWD or one | |
| // of its ancestors has been unlinked. | |
| if (!parent) { | |
| return -ENOENT; | |
| } | |
| auto name = parent->locked().getName(curr); | |
| result = '/' + name + result; | |
| curr = parent; | |
| } | |
| // Check if the cwd is the root directory. | |
| if (result.empty()) { | |
| result = "/"; | |
| } | |
| int len = result.length() + 1; | |
| // Check if the size argument is less than the length of the absolute | |
| // pathname of the working directory, including null terminator. | |
| if (len > size) { | |
| return -ERANGE; | |
| } | |
| // Return value is a null-terminated c string. | |
| strcpy((char*)buf, result.c_str()); | |
| return len; | |
| } | |
| __wasi_errno_t __wasi_fd_fdstat_get(__wasi_fd_t fd, __wasi_fdstat_t* stat) { | |
| // TODO: This is only partial implementation of __wasi_fd_fdstat_get. Enough | |
| // to get __wasi_fd_is_valid working. | |
| // There are other fields in the stat structure that we should really | |
| // be filling in here. | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return __WASI_ERRNO_BADF; | |
| } | |
| if (openFile->locked().getFile()->is<Directory>()) { | |
| stat->fs_filetype = __WASI_FILETYPE_DIRECTORY; | |
| } else { | |
| stat->fs_filetype = __WASI_FILETYPE_REGULAR_FILE; | |
| } | |
| return __WASI_ERRNO_SUCCESS; | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_unlinkat(int dirfd, intptr_t path, int flags) { | |
| if (flags & ~AT_REMOVEDIR) { | |
| // TODO: Test this case. | |
| return -EINVAL; | |
| } | |
| // It is invalid for rmdir paths to end in ".", but we need to distinguish | |
| // this case from the case of `parseParent` returning (root, '.') when parsing | |
| // "/", so we need to find the invalid "/." manually. | |
| if (flags == AT_REMOVEDIR) { | |
| std::string_view p((char*)path); | |
| // Ignore trailing '/'. | |
| while (!p.empty() && p.back() == '/') { | |
| p.remove_suffix(1); | |
| } | |
| if (p.size() >= 2 && p.substr(p.size() - 2) == std::string_view("/.")) { | |
| return -EINVAL; | |
| } | |
| } | |
| auto parsed = path::parseParent((char*)path, dirfd); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto& [parent, childNameView] = parsed.getParentChild(); | |
| std::string childName(childNameView); | |
| auto lockedParent = parent->locked(); | |
| auto file = lockedParent.getChild(childName); | |
| if (!file) { | |
| return -ENOENT; | |
| } | |
| // Disallow removing the root directory, even if it is empty. | |
| if (file == wasmFS.getRootDirectory()) { | |
| return -EBUSY; | |
| } | |
| auto lockedFile = file->locked(); | |
| if (auto dir = file->dynCast<Directory>()) { | |
| if (flags != AT_REMOVEDIR) { | |
| return -EISDIR; | |
| } | |
| // A directory can only be removed if it has no entries. | |
| if (dir->locked().getNumEntries() > 0) { | |
| return -ENOTEMPTY; | |
| } | |
| } else { | |
| // A normal file or symlink. | |
| if (flags == AT_REMOVEDIR) { | |
| return -ENOTDIR; | |
| } | |
| } | |
| // Cannot unlink/rmdir if the parent dir doesn't have write permissions. | |
| if (!(lockedParent.getMode() & WASMFS_PERM_WRITE)) { | |
| return -EACCES; | |
| } | |
| // Input is valid, perform the unlink. | |
| return lockedParent.removeChild(childName); | |
| } | |
| int __syscall_rmdir(intptr_t path) { | |
| return __syscall_unlinkat(AT_FDCWD, path, AT_REMOVEDIR); | |
| } | |
| // wasmfs_unmount is similar to __syscall_unlinkat, but assumes AT_REMOVEDIR is | |
| // true and will only unlink mountpoints (Empty and nonempty). | |
| int wasmfs_unmount(const char* path) { | |
| auto parsed = path::parseParent(path, AT_FDCWD); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto& [parent, childNameView] = parsed.getParentChild(); | |
| std::string childName(childNameView); | |
| auto lockedParent = parent->locked(); | |
| auto file = lockedParent.getChild(childName); | |
| if (!file) { | |
| return -ENOENT; | |
| } | |
| // Disallow removing the root directory, even if it is empty. | |
| if (file == wasmFS.getRootDirectory()) { | |
| return -EBUSY; | |
| } | |
| if (!file->dynCast<Directory>()) { | |
| // A normal file or symlink. | |
| return -ENOTDIR; | |
| } | |
| if (parent->getBackend() == file->getBackend()) { | |
| // The child is not a valid mountpoint. | |
| return -EINVAL; | |
| } | |
| // Input is valid, perform the unlink. | |
| return lockedParent.removeChild(childName); | |
| } | |
| int __syscall_getdents64(int fd, intptr_t dirp, size_t count) { | |
| dirent* result = (dirent*)dirp; | |
| // Check if the result buffer is too small. | |
| if (count / sizeof(dirent) == 0) { | |
| return -EINVAL; | |
| } | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| auto lockedOpenFile = openFile->locked(); | |
| auto dir = lockedOpenFile.getFile()->dynCast<Directory>(); | |
| if (!dir) { | |
| return -ENOTDIR; | |
| } | |
| auto lockedDir = dir->locked(); | |
| // A directory's position corresponds to the index in its entries vector. | |
| int index = lockedOpenFile.getPosition(); | |
| // If this directory has been unlinked and has no parent, then it is | |
| // completely empty. | |
| auto parent = lockedDir.getParent(); | |
| if (!parent) { | |
| return 0; | |
| } | |
| off_t bytesRead = 0; | |
| const auto& dirents = openFile->dirents; | |
| for (; index < dirents.size() && bytesRead + sizeof(dirent) <= count; | |
| index++) { | |
| const auto& entry = dirents[index]; | |
| result->d_ino = entry.ino; | |
| result->d_off = index + 1; | |
| result->d_reclen = sizeof(dirent); | |
| switch (entry.kind) { | |
| case File::UnknownKind: | |
| result->d_type = DT_UNKNOWN; | |
| break; | |
| case File::DataFileKind: | |
| result->d_type = DT_REG; | |
| break; | |
| case File::DirectoryKind: | |
| result->d_type = DT_DIR; | |
| break; | |
| case File::SymlinkKind: | |
| result->d_type = DT_LNK; | |
| break; | |
| default: | |
| result->d_type = DT_UNKNOWN; | |
| break; | |
| } | |
| assert(entry.name.size() + 1 <= sizeof(result->d_name)); | |
| strcpy(result->d_name, entry.name.c_str()); | |
| ++result; | |
| bytesRead += sizeof(dirent); | |
| } | |
| // Update position | |
| lockedOpenFile.setPosition(index); | |
| return bytesRead; | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_renameat(int olddirfd, | |
| intptr_t oldpath, | |
| int newdirfd, | |
| intptr_t newpath) { | |
| // Rename is the only syscall that needs to (or is allowed to) acquire locks | |
| // on two directories at once. It requires locks on both the old and new | |
| // parent directories to ensure that the moved file can be atomically removed | |
| // from the old directory and added to the new directory without something | |
| // changing that would prevent the move. | |
| // | |
| // To prevent deadlock in the case of simultaneous renames, serialize renames | |
| // with an additional global lock. | |
| static std::mutex renameMutex; | |
| std::lock_guard<std::mutex> renameLock(renameMutex); | |
| // Get the old directory. | |
| auto parsedOld = path::parseParent((char*)oldpath, olddirfd); | |
| if (auto err = parsedOld.getError()) { | |
| return err; | |
| } | |
| auto& [oldParent, oldFileNameView] = parsedOld.getParentChild(); | |
| std::string oldFileName(oldFileNameView); | |
| // Get the new directory. | |
| auto parsedNew = path::parseParent((char*)newpath, newdirfd); | |
| if (auto err = parsedNew.getError()) { | |
| return err; | |
| } | |
| auto& [newParent, newFileNameView] = parsedNew.getParentChild(); | |
| std::string newFileName(newFileNameView); | |
| if (newFileNameView.size() > WASMFS_NAME_MAX) { | |
| return -ENAMETOOLONG; | |
| } | |
| // Lock both directories. | |
| auto lockedOldParent = oldParent->locked(); | |
| auto lockedNewParent = newParent->locked(); | |
| // Get the source and destination files. | |
| auto oldFile = lockedOldParent.getChild(oldFileName); | |
| auto newFile = lockedNewParent.getChild(newFileName); | |
| if (!oldFile) { | |
| return -ENOENT; | |
| } | |
| // If the source and destination are the same, do nothing. | |
| if (oldFile == newFile) { | |
| return 0; | |
| } | |
| // Never allow renaming or overwriting the root. | |
| auto root = wasmFS.getRootDirectory(); | |
| if (oldFile == root || newFile == root) { | |
| return -EBUSY; | |
| } | |
| // Cannot modify either directory without write permissions. | |
| if (!(lockedOldParent.getMode() & WASMFS_PERM_WRITE) || | |
| !(lockedNewParent.getMode() & WASMFS_PERM_WRITE)) { | |
| return -EACCES; | |
| } | |
| // Both parents must have the same backend. | |
| if (oldParent->getBackend() != newParent->getBackend()) { | |
| return -EXDEV; | |
| } | |
| // Check that oldDir is not an ancestor of newDir. | |
| for (auto curr = newParent; curr != root; curr = curr->locked().getParent()) { | |
| if (curr == oldFile) { | |
| return -EINVAL; | |
| } | |
| } | |
| // The new file will be removed if it already exists. | |
| if (newFile) { | |
| if (auto newDir = newFile->dynCast<Directory>()) { | |
| // Cannot overwrite a directory with a non-directory. | |
| auto oldDir = oldFile->dynCast<Directory>(); | |
| if (!oldDir) { | |
| return -EISDIR; | |
| } | |
| // Cannot overwrite a non-empty directory. | |
| if (newDir->locked().getNumEntries() > 0) { | |
| return -ENOTEMPTY; | |
| } | |
| } else { | |
| // Cannot overwrite a non-directory with a directory. | |
| if (oldFile->is<Directory>()) { | |
| return -ENOTDIR; | |
| } | |
| } | |
| } | |
| // Perform the move. | |
| if (auto err = lockedNewParent.insertMove(newFileName, oldFile)) { | |
| assert(err < 0); | |
| return err; | |
| } | |
| return 0; | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_symlinkat(intptr_t target, int newdirfd, intptr_t linkpath) { | |
| auto parsed = path::parseParent((char*)linkpath, newdirfd); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto& [parent, childNameView] = parsed.getParentChild(); | |
| if (childNameView.size() > WASMFS_NAME_MAX) { | |
| return -ENAMETOOLONG; | |
| } | |
| auto lockedParent = parent->locked(); | |
| std::string childName(childNameView); | |
| if (lockedParent.getChild(childName)) { | |
| return -EEXIST; | |
| } | |
| if (!lockedParent.insertSymlink(childName, (char*)target)) { | |
| return -EPERM; | |
| } | |
| return 0; | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_readlinkat(int dirfd, | |
| intptr_t path, | |
| intptr_t buf, | |
| size_t bufsize) { | |
| // TODO: Handle empty paths. | |
| auto parsed = path::parseFile((char*)path, dirfd, path::NoFollowLinks); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto link = parsed.getFile()->dynCast<Symlink>(); | |
| if (!link) { | |
| return -EINVAL; | |
| } | |
| const auto& target = link->getTarget(); | |
| auto bytes = std::min((size_t)bufsize, target.size()); | |
| memcpy((char*)buf, target.c_str(), bytes); | |
| return bytes; | |
| } | |
| static double timespec_to_ms(timespec ts) { | |
| if (ts.tv_nsec == UTIME_OMIT) { | |
| return INFINITY; | |
| } | |
| if (ts.tv_nsec == UTIME_NOW) { | |
| return emscripten_date_now(); | |
| } | |
| return double(ts.tv_sec) * 1000 + double(ts.tv_nsec) / (1000 * 1000); | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_utimensat(int dirFD, intptr_t path_, intptr_t times_, int flags) { | |
| const char* path = (const char*)path_; | |
| const struct timespec* times = (const struct timespec*)times_; | |
| if (flags & ~AT_SYMLINK_NOFOLLOW) { | |
| // TODO: Test this case. | |
| return -EINVAL; | |
| } | |
| // Add AT_EMPTY_PATH as Linux (and so, musl, and us) has a nonstandard | |
| // behavior in which an empty path means to operate on whatever is in dirFD | |
| // (directory or not), which is exactly the behavior of AT_EMPTY_PATH (but | |
| // without passing that in). See "C library/kernel ABI differences" in | |
| // https://man7.org/linux/man-pages/man2/utimensat.2.html | |
| // | |
| // TODO: Handle AT_SYMLINK_NOFOLLOW once we traverse symlinks correctly. | |
| auto parsed = path::getFileAt(dirFD, path, flags | AT_EMPTY_PATH); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| // TODO: Handle tv_nsec being UTIME_NOW or UTIME_OMIT. | |
| // TODO: Check for write access to the file (see man page for specifics). | |
| double aTime, mTime; | |
| if (times == nullptr) { | |
| aTime = mTime = emscripten_date_now(); | |
| } else { | |
| aTime = timespec_to_ms(times[0]); | |
| mTime = timespec_to_ms(times[1]); | |
| } | |
| auto locked = parsed.getFile()->locked(); | |
| if (aTime != INFINITY) { | |
| locked.setATime(aTime); | |
| } | |
| if (mTime != INFINITY) { | |
| locked.setMTime(mTime); | |
| } | |
| return 0; | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_fchmodat2(int dirfd, intptr_t path, int mode, int flags) { | |
| if (flags & ~AT_SYMLINK_NOFOLLOW) { | |
| // TODO: Test this case. | |
| return -EINVAL; | |
| } | |
| auto parsed = path::getFileAt(dirfd, (char*)path, flags); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| auto lockedFile = parsed.getFile()->locked(); | |
| lockedFile.setMode(mode); | |
| // On POSIX, ctime is updated on metadata changes, like chmod. | |
| lockedFile.updateCTime(); | |
| return 0; | |
| } | |
| int __syscall_chmod(intptr_t path, int mode) { | |
| return __syscall_fchmodat2(AT_FDCWD, path, mode, 0); | |
| } | |
| int __syscall_fchmod(int fd, int mode) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| auto lockedFile = openFile->locked().getFile()->locked(); | |
| lockedFile.setMode(mode); | |
| lockedFile.updateCTime(); | |
| return 0; | |
| } | |
| int __syscall_fchownat( | |
| int dirfd, intptr_t path, int owner, int group, int flags) { | |
| // Only accept valid flags. | |
| if (flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) { | |
| // TODO: Test this case. | |
| return -EINVAL; | |
| } | |
| auto parsed = path::getFileAt(dirfd, (char*)path, flags); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| // Ignore the actual owner and group because we don't track those. | |
| // TODO: Update metadata time stamp. | |
| return 0; | |
| } | |
| int __syscall_fchown32(int fd, int owner, int group) { | |
| return __syscall_fchownat(fd, (intptr_t) "", owner, group, AT_EMPTY_PATH); | |
| } | |
| // TODO: Test this with non-AT_FDCWD values. | |
| int __syscall_faccessat(int dirfd, intptr_t path, int amode, int flags) { | |
| // The input must be F_OK (check for existence) or a combination of [RWX]_OK | |
| // flags. | |
| if (amode != F_OK && (amode & ~(R_OK | W_OK | X_OK))) { | |
| return -EINVAL; | |
| } | |
| if (flags & ~(AT_EACCESS | AT_SYMLINK_NOFOLLOW)) { | |
| // TODO: Test this case. | |
| return -EINVAL; | |
| } | |
| // TODO: Handle AT_SYMLINK_NOFOLLOW once we traverse symlinks correctly. | |
| auto parsed = path::parseFile((char*)path, dirfd); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| if (amode != F_OK) { | |
| auto mode = parsed.getFile()->locked().getMode(); | |
| if ((amode & R_OK) && !(mode & WASMFS_PERM_READ)) { | |
| return -EACCES; | |
| } | |
| if ((amode & W_OK) && !(mode & WASMFS_PERM_WRITE)) { | |
| return -EACCES; | |
| } | |
| if ((amode & X_OK) && !(mode & WASMFS_PERM_EXECUTE)) { | |
| return -EACCES; | |
| } | |
| } | |
| return 0; | |
| } | |
| static int doTruncate(std::shared_ptr<File>& file, off_t size) { | |
| auto dataFile = file->dynCast<DataFile>(); | |
| if (!dataFile) { | |
| return -EISDIR; | |
| } | |
| auto locked = dataFile->locked(); | |
| if (!(locked.getMode() & WASMFS_PERM_WRITE)) { | |
| return -EACCES; | |
| } | |
| if (size < 0) { | |
| return -EINVAL; | |
| } | |
| int ret = locked.setSize(size); | |
| assert(ret <= 0); | |
| return ret; | |
| } | |
| int __syscall_truncate64(intptr_t path, off_t size) { | |
| auto parsed = path::parseFile((char*)path); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| return doTruncate(parsed.getFile(), size); | |
| } | |
| int __syscall_ftruncate64(int fd, off_t size) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| auto ret = doTruncate(openFile->locked().getFile(), size); | |
| // XXX It is not clear from the docs why ftruncate would differ from | |
| // truncate here. However, on Linux this definitely happens, and the old | |
| // FS matches that as well, so do the same here. | |
| if (ret == -EACCES) { | |
| ret = -EINVAL; | |
| } | |
| return ret; | |
| } | |
| static bool isTTY(std::shared_ptr<File>& file) { | |
| // TODO: Full TTY support. For now, just see stdin/out/err as terminals and | |
| // nothing else. | |
| return file == SpecialFiles::getStdin() || | |
| file == SpecialFiles::getStdout() || file == SpecialFiles::getStderr(); | |
| } | |
| int __syscall_ioctl(int fd, int request, ...) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| va_list args; | |
| va_start(args, request); | |
| void* argp = va_arg(args, void*); | |
| va_end(args); | |
| auto openHandle = openFile->locked(); | |
| auto file = openHandle.getFile(); | |
| if (request == FIONREAD) { | |
| off_t size = file->locked().getSize(); | |
| if (size < 0) { | |
| return (int)size; | |
| } | |
| if (file->isSeekable()) { | |
| size -= openHandle.getPosition(); | |
| } | |
| *static_cast<int*>(argp) = static_cast<int>(size); | |
| return 0; | |
| } | |
| if (!isTTY(file)) { | |
| return -ENOTTY; | |
| } | |
| // TODO: Full TTY support. For now this is limited, and matches the old FS. | |
| switch (request) { | |
| case TCGETA: | |
| case TCGETS: | |
| case TCSETA: | |
| case TCSETAW: | |
| case TCSETAF: | |
| case TCSETS: | |
| case TCSETSW: | |
| case TCSETSF: | |
| case TIOCGWINSZ: | |
| case TIOCSWINSZ: { | |
| // TTY operations that we do nothing for anyhow can just be ignored. | |
| return 0; | |
| } | |
| default: { | |
| return -EINVAL; // not supported | |
| } | |
| } | |
| } | |
| int __syscall_pipe2(intptr_t fd, int flags) { | |
| auto* fds = (__wasi_fd_t*)fd; | |
| if (flags && flags != O_CLOEXEC) { | |
| return -ENOTSUP; | |
| } | |
| // Make a pipe: Two PipeFiles that share a single data source between them, so | |
| // that writing to one can be read in the other. | |
| // | |
| // No backend is needed here, so pass in nullptr for that. | |
| auto data = std::make_shared<PipeData>(); | |
| auto reader = std::make_shared<PipeFile>(S_IRUGO, data); | |
| auto writer = std::make_shared<PipeFile>(S_IWUGO, data); | |
| std::shared_ptr<OpenFileState> openReader, openWriter; | |
| (void)OpenFileState::create(reader, O_RDONLY, openReader); | |
| (void)OpenFileState::create(writer, O_WRONLY, openWriter); | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| fds[0] = fileTable.addEntry(openReader); | |
| fds[1] = fileTable.addEntry(openWriter); | |
| return 0; | |
| } | |
| // int poll(struct pollfd* fds, nfds_t nfds, int timeout); | |
| int __syscall_poll(intptr_t fds_, int nfds, int timeout) { | |
| struct pollfd* fds = (struct pollfd*)fds_; | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| // Process the list of FDs and compute their revents masks. Count the number | |
| // of nonzero such masks, which is our return value. | |
| int nonzero = 0; | |
| for (nfds_t i = 0; i < nfds; i++) { | |
| auto* pollfd = &fds[i]; | |
| auto fd = pollfd->fd; | |
| if (fd < 0) { | |
| // Negative FDs are ignored in poll(). | |
| pollfd->revents = 0; | |
| continue; | |
| } | |
| // Assume invalid, unless there is an open file. | |
| auto mask = POLLNVAL; | |
| auto openFile = fileTable.getEntry(fd); | |
| if (openFile) { | |
| mask = 0; | |
| auto flags = openFile->locked().getFlags(); | |
| auto accessMode = flags & O_ACCMODE; | |
| auto readBit = pollfd->events & POLLOUT; | |
| if (readBit && (accessMode == O_WRONLY || accessMode == O_RDWR)) { | |
| mask |= readBit; | |
| } | |
| auto writeBit = pollfd->events & POLLIN; | |
| if (writeBit && (accessMode == O_RDONLY || accessMode == O_RDWR)) { | |
| // If there is data in the file, then there is also the ability to read. | |
| // TODO: Does this need to consider the position as well? That is, if | |
| // the position is at the end, we can't read from the current position | |
| // at least. If we update this, make sure the size isn't an error! | |
| if (openFile->locked().getFile()->locked().getSize() > 0) { | |
| mask |= writeBit; | |
| } | |
| } | |
| // TODO: get mask from File dynamically using a poll() hook? | |
| } | |
| // TODO: set the state based on the state of the other end of the pipe, for | |
| // pipes (POLLERR | POLLHUP) | |
| if (mask) { | |
| nonzero++; | |
| } | |
| pollfd->revents = mask; | |
| } | |
| // TODO: This should block based on the timeout. The old FS did not do so due | |
| // to web limitations, which we should perhaps revisit (especially with | |
| // pthreads and asyncify). | |
| return nonzero; | |
| } | |
| int __syscall_fallocate(int fd, int mode, off_t offset, off_t len) { | |
| assert(mode == 0); // TODO, but other modes were never supported in the old FS | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| auto openFile = fileTable.getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| auto dataFile = openFile->locked().getFile()->dynCast<DataFile>(); | |
| // TODO: support for symlinks. | |
| if (!dataFile) { | |
| return -ENODEV; | |
| } | |
| auto locked = dataFile->locked(); | |
| if (!(locked.getMode() & WASMFS_PERM_WRITE)) { | |
| return -EBADF; | |
| } | |
| if (offset < 0 || len <= 0) { | |
| return -EINVAL; | |
| } | |
| // TODO: We could only fill zeros for regions that were completely unused | |
| // before, which for a backend with sparse data storage could make a | |
| // difference. For that we'd need a new backend API. | |
| auto newNeededSize = offset + len; | |
| off_t size = locked.getSize(); | |
| if (size < 0) { | |
| return size; | |
| } | |
| if (newNeededSize > size) { | |
| if (auto err = locked.setSize(newNeededSize)) { | |
| assert(err < 0); | |
| return err; | |
| } | |
| } | |
| return 0; | |
| } | |
| int __syscall_fcntl64(int fd, int cmd, ...) { | |
| auto fileTable = wasmFS.getFileTable().locked(); | |
| auto openFile = fileTable.getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| switch (cmd) { | |
| case F_DUPFD: { | |
| int newfd; | |
| va_list v1; | |
| va_start(v1, cmd); | |
| newfd = va_arg(v1, int); | |
| va_end(v1); | |
| if (newfd < 0) { | |
| return -EINVAL; | |
| } | |
| // Find the first available fd at arg or after. | |
| // TODO: Should we check for a limit on the max FD number, if we have one? | |
| while (1) { | |
| if (!fileTable.getEntry(newfd)) { | |
| (void)fileTable.setEntry(newfd, openFile); | |
| return newfd; | |
| } | |
| newfd++; | |
| } | |
| } | |
| case F_GETFD: | |
| case F_SETFD: | |
| // FD_CLOEXEC makes no sense for a single process. | |
| return 0; | |
| case F_GETFL: | |
| return openFile->locked().getFlags(); | |
| case F_SETFL: { | |
| int flags; | |
| va_list v1; | |
| va_start(v1, cmd); | |
| flags = va_arg(v1, int); | |
| va_end(v1); | |
| auto lockedOpenFile = openFile->locked(); | |
| auto oldFlags = lockedOpenFile.getFlags(); | |
| // This syscall should ignore most flags. | |
| int mask = O_APPEND | O_NONBLOCK | O_ASYNC | O_DIRECT | O_NOATIME; | |
| lockedOpenFile.setFlags((oldFlags & ~mask) | (flags & mask)); | |
| return 0; | |
| } | |
| case F_GETLK: { | |
| // If these constants differ then we'd need a case for both. | |
| static_assert(F_GETLK == F_GETLK64); | |
| flock* data; | |
| va_list v1; | |
| va_start(v1, cmd); | |
| data = va_arg(v1, flock*); | |
| va_end(v1); | |
| // We're always unlocked for now, until we implement byte-range locks. | |
| data->l_type = F_UNLCK; | |
| return 0; | |
| } | |
| case F_SETLK: | |
| case F_SETLKW: { | |
| static_assert(F_SETLK == F_SETLK64); | |
| static_assert(F_SETLKW == F_SETLKW64); | |
| // Pretend that the locking is successful. These are process-level locks, | |
| // and Emscripten programs are a single process. If we supported linking a | |
| // filesystem between programs, we'd need to do more here. | |
| // See https://github.com/emscripten-core/emscripten/issues/23697 | |
| return 0; | |
| } | |
| default: { | |
| // TODO: support any remaining cmds | |
| return -EINVAL; | |
| } | |
| } | |
| } | |
| static int | |
| doStatFS(std::shared_ptr<File>& file, size_t size, struct statfs* buf) { | |
| if (size != sizeof(struct statfs)) { | |
| // We only know how to write to a standard statfs, not even a truncated one. | |
| return -EINVAL; | |
| } | |
| // NOTE: None of the constants here are true. We're just returning safe and | |
| // sane values, that match the long-existing JS FS behavior (except for | |
| // the inode number, where we can do better). | |
| buf->f_type = 0; | |
| buf->f_bsize = 4096; | |
| buf->f_frsize = 4096; | |
| buf->f_blocks = 1000000; | |
| buf->f_bfree = 500000; | |
| buf->f_bavail = 500000; | |
| buf->f_files = file->getIno(); | |
| buf->f_ffree = 1000000; | |
| buf->f_fsid = {0, 0}; | |
| buf->f_flags = ST_NOSUID; | |
| buf->f_namelen = 255; | |
| return 0; | |
| } | |
| int __syscall_statfs64(intptr_t path, size_t size, intptr_t buf) { | |
| auto parsed = path::parseFile((char*)path); | |
| if (auto err = parsed.getError()) { | |
| return err; | |
| } | |
| return doStatFS(parsed.getFile(), size, (struct statfs*)buf); | |
| } | |
| int __syscall_fstatfs64(int fd, size_t size, intptr_t buf) { | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| return doStatFS(openFile->locked().getFile(), size, (struct statfs*)buf); | |
| } | |
| int _mmap_js(size_t length, | |
| int prot, | |
| int flags, | |
| int fd, | |
| off_t offset, | |
| int* allocated, | |
| void** addr) { | |
| // PROT_EXEC is not supported (although we pretend to support the absence of | |
| // PROT_READ or PROT_WRITE). | |
| if ((prot & PROT_EXEC)) { | |
| return -EPERM; | |
| } | |
| if (!length) { | |
| return -EINVAL; | |
| } | |
| // One of MAP_PRIVATE, MAP_SHARED, or MAP_SHARED_VALIDATE must be used. | |
| int mapType = flags & MAP_TYPE; | |
| if (mapType != MAP_PRIVATE && mapType != MAP_SHARED && | |
| mapType != MAP_SHARED_VALIDATE) { | |
| return -EINVAL; | |
| } | |
| if (mapType == MAP_SHARED_VALIDATE) { | |
| WASMFS_UNREACHABLE("TODO: MAP_SHARED_VALIDATE"); | |
| } | |
| auto openFile = wasmFS.getFileTable().locked().getEntry(fd); | |
| if (!openFile) { | |
| return -EBADF; | |
| } | |
| std::shared_ptr<DataFile> file; | |
| // Keep the open file info locked only for as long as we need that. | |
| { | |
| auto lockedOpenFile = openFile->locked(); | |
| // Check permissions. We always need read permissions, since we need to read | |
| // the data in the file to map it. | |
| if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_WRONLY) { | |
| return -EACCES; | |
| } | |
| // According to the POSIX spec it is possible to write to a file opened in | |
| // read-only mode with MAP_PRIVATE flag, as all modifications will be | |
| // visible only in the memory of the current process. | |
| if ((prot & PROT_WRITE) != 0 && mapType != MAP_PRIVATE && | |
| (lockedOpenFile.getFlags() & O_ACCMODE) != O_RDWR) { | |
| return -EACCES; | |
| } | |
| file = lockedOpenFile.getFile()->dynCast<DataFile>(); | |
| } | |
| if (!file) { | |
| return -ENODEV; | |
| } | |
| // TODO: On MAP_SHARED, install the mapping on the DataFile object itself so | |
| // that reads and writes can be redirected to the mapped region and so that | |
| // the mapping can correctly outlive the file being closed. This will require | |
| // changes to emscripten_mmap.c as well. | |
| // Align to a wasm page size, as we expect in the future to get wasm | |
| // primitives to do this work, and those would presumably be aligned to a page | |
| // size. Aligning now avoids confusion later. | |
| uint8_t* ptr = (uint8_t*)emscripten_builtin_memalign(WASM_PAGE_SIZE, length); | |
| if (!ptr) { | |
| return -ENOMEM; | |
| } | |
| auto nread = file->locked().read(ptr, length, offset); | |
| if (nread < 0) { | |
| // The read failed. Report the error, but first free the allocation. | |
| emscripten_builtin_free(ptr); | |
| return nread; | |
| } | |
| // From here on, we have succeeded, and can mark the allocation as having | |
| // occurred (which means that the caller has the responsibility to free it). | |
| *allocated = true; | |
| *addr = (void*)ptr; | |
| // The read must be of a valid amount, or we have had an internal logic error. | |
| assert(nread <= length); | |
| // mmap clears any extra bytes after the data itself. | |
| memset(ptr + nread, 0, length - nread); | |
| return 0; | |
| } | |
| int _msync_js( | |
| intptr_t addr, size_t length, int prot, int flags, int fd, off_t offset) { | |
| // TODO: This is not correct! Mappings should be associated with files, not | |
| // fds. Only need to sync if shared and writes are allowed. | |
| int mapType = flags & MAP_TYPE; | |
| if (mapType == MAP_SHARED && (prot & PROT_WRITE)) { | |
| __wasi_ciovec_t iovec; | |
| iovec.buf = (uint8_t*)addr; | |
| iovec.buf_len = length; | |
| __wasi_size_t nwritten; | |
| // Translate from WASI positive error codes to negative error codes. | |
| return -__wasi_fd_pwrite(fd, &iovec, 1, offset, &nwritten); | |
| } | |
| return 0; | |
| } | |
| int _munmap_js( | |
| intptr_t addr, size_t length, int prot, int flags, int fd, off_t offset) { | |
| // TODO: This is not correct! Mappings should be associated with files, not | |
| // fds. | |
| // TODO: Syncing should probably be handled in __syscall_munmap instead. | |
| return _msync_js(addr, length, prot, flags, fd, offset); | |
| } | |
| // Stubs (at least for now) | |
| int __syscall_accept4(int sockfd, | |
| intptr_t addr, | |
| intptr_t addrlen, | |
| int flags, | |
| int dummy1, | |
| int dummy2) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_bind( | |
| int sockfd, intptr_t addr, size_t alen, int dummy, int dummy2, int dummy3) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_connect( | |
| int sockfd, intptr_t addr, size_t len, int dummy, int dummy2, int dummy3) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_socket( | |
| int domain, int type, int protocol, int dummy1, int dummy2, int dummy3) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_listen( | |
| int sockfd, int backlog, int dummy1, int dummy2, int dummy3, int dummy4) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_getsockopt(int sockfd, | |
| int level, | |
| int optname, | |
| intptr_t optval, | |
| intptr_t optlen, | |
| int dummy) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_getsockname( | |
| int sockfd, intptr_t addr, intptr_t len, int dummy, int dummy2, int dummy3) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_getpeername( | |
| int sockfd, intptr_t addr, intptr_t len, int dummy, int dummy2, int dummy3) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_sendto( | |
| int sockfd, intptr_t msg, size_t len, int flags, intptr_t addr, size_t alen) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_sendmsg( | |
| int sockfd, intptr_t msg, int flags, intptr_t addr, size_t alen, int dummy) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_recvfrom(int sockfd, | |
| intptr_t msg, | |
| size_t len, | |
| int flags, | |
| intptr_t addr, | |
| intptr_t alen) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_recvmsg( | |
| int sockfd, intptr_t msg, int flags, int dummy, int dummy2, int dummy3) { | |
| return -ENOSYS; | |
| } | |
| int __syscall_fadvise64(int fd, off_t offset, off_t length, int advice) { | |
| // Advice is currently ignored. TODO some backends might use it | |
| return 0; | |
| } | |
| } // extern "C" | |
Xet Storage Details
- Size:
- 53 kB
- Xet hash:
- 544903244841051f082b7c5f19ac1dbb4bc0a8a048600a8406cd5854c599ccdc
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.