diff --git a/.circleci/config.yml b/.circleci/config.yml index ccf22f95cc823..5ae385f66c9f3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -494,7 +494,7 @@ jobs: name: get wasmtime command: | # use a pinned version due to https://github.com/bytecodealliance/wasmtime/issues/714 - export VERSION=v0.33.0 + export VERSION=v32.0.0 wget https://github.com/bytecodealliance/wasmtime/releases/download/$VERSION/wasmtime-$VERSION-x86_64-linux.tar.xz tar -xf wasmtime-$VERSION-x86_64-linux.tar.xz cp wasmtime-$VERSION-x86_64-linux/wasmtime ~/vms diff --git a/LICENSE b/LICENSE index 70cabe388d27d..c973e2ae71cb6 100644 --- a/LICENSE +++ b/LICENSE @@ -100,3 +100,31 @@ The third_party/ subdirectory contains code with other licenses. None of it is used by default, but certain options use it (e.g., the optional closure compiler flag will run closure compiler from third_party/). +Files in system/lib/standalone/ contain code derived from wasi-libc, in +accordance with the terms of the MIT license. wasi-libc's license follows: + + """ + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + """ diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index c5474f4847718..58f393ffa2ad2 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -969,6 +969,7 @@ sigs = { fd_pread__sig: 'iippjp', fd_pwrite__sig: 'iippjp', fd_read__sig: 'iippp', + fd_readdir__sig: 'iippjp', fd_seek__sig: 'iijip', fd_sync__sig: 'ii', fd_write__sig: 'iippp', @@ -1536,6 +1537,9 @@ sigs = { lineColor__sig: 'ipiiiii', lineRGBA__sig: 'ipiiiiiiii', llvm_eh_typeid_for__sig: 'vp', + path_create_directory__sig: 'iipp', + path_filestat_get__sig: 'iiippp', + path_symlink__sig: 'ippipp', pixelRGBA__sig: 'ipiiiiii', proc_exit__sig: 'vi', random_get__sig: 'ipp', diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index de874ccf868ca..47eaf86ad4064 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -612,6 +612,26 @@ var WasiLibrary = { randomFill(HEAPU8.subarray(buffer, buffer + size)); return 0; }, + + path_filestat_get__nothrow: true, + path_filestat_get: (fd, flags, path, path_len, buf) => { + return {{{ cDefs.ENOSYS }}}; + }, + + path_create_directory__nothrow: true, + path_create_directory: (fd, path, path_len) => { + return {{{ cDefs.ENOSYS }}}; + }, + + path_symlink__nothrow: true, + path_symlink: (old_path, old_path_len, fd, new_path, new_path_len) => { + return {{{ cDefs.ENOSYS }}}; + }, + + fd_readdir__nothrow: true, + fd_readdir: (fd, buf, buf_len, cookie, bufused) => { + return {{{ cDefs.ENOSYS }}}; + }, }; for (var x in WasiLibrary) { diff --git a/system/lib/libc/musl/src/dirent/seekdir.c b/system/lib/libc/musl/src/dirent/seekdir.c index bf6cc6ec40453..5c0b0cfab495b 100644 --- a/system/lib/libc/musl/src/dirent/seekdir.c +++ b/system/lib/libc/musl/src/dirent/seekdir.c @@ -7,6 +7,11 @@ void seekdir(DIR *dir, long off) { LOCK(dir->lock); dir->tell = lseek(dir->fd, off, SEEK_SET); +#if defined(__EMSCRIPTEN__) + // The above relies on `lseek` to work on a directory fd, which is not + // guaranteed on WASI. Just set `off` again if the above failed. + dir->tell = off; +#endif dir->buf_pos = dir->buf_end = 0; UNLOCK(dir->lock); } diff --git a/system/lib/standalone/paths.c b/system/lib/standalone/paths.c new file mode 100644 index 0000000000000..db169b5703b9b --- /dev/null +++ b/system/lib/standalone/paths.c @@ -0,0 +1,617 @@ +#define _GNU_SOURCE +#include "paths.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "lock.h" + +/// A name and file descriptor pair. +typedef struct preopen { + /// The path prefix associated with the file descriptor. + const char* prefix; + + /// The file descriptor. + __wasi_fd_t fd; + + /// Device ID of device containing the file. + __wasi_device_t dev; + + /// File serial number. + __wasi_inode_t ino; +} preopen; + +/// A simple growable array of `preopen`. +static preopen* preopens; +static size_t num_preopens; + +/// cwd handling +static bool cwd_is_root = true; +static __wasi_fd_t cwd_fd; +static bool cwd_fd_from_preopen; + +/// Access to the cwd above must be protected. +static volatile int lock[1]; + +/// Are the `prefix_len` bytes pointed to by `prefix` a prefix of `path`? +static bool +prefix_matches(const char* prefix, size_t prefix_len, const char* path) { + // Allow an empty string as a prefix of any relative path. + if (path[0] != '/' && prefix_len == 0) + return true; + + // Check whether any bytes of the prefix differ. + if (memcmp(path, prefix, prefix_len) != 0) + return false; + + // Ignore trailing slashes in directory names. + size_t i = prefix_len; + while (i > 0 && prefix[i - 1] == '/') { + --i; + } + + // Match only complete path components. + char last = path[i]; + return last == '/' || last == '\0'; +} + +static bool resolve_path_unlocked(bool* need_unlock, + int* resolved_dirfd, + const char** path_ptr) { + const char* path = *path_ptr; + + if (path[0] == '\0') { + return false; + } + + if (*resolved_dirfd != AT_FDCWD && path[0] != '/') { + *need_unlock = false; + return true; + } + + bool is_absolute = path[0] == '/'; + + // Strip leading `/` characters, the prefixes we're mataching won't have + // them. + while (*path == '/') + path++; + + if (!cwd_is_root && !is_absolute) { + assert(*resolved_dirfd == AT_FDCWD); + + *need_unlock = !cwd_fd_from_preopen; + *resolved_dirfd = cwd_fd; + *path_ptr = path; + return true; + } + + // Search through the preopens table. Iterate in reverse so that more + // recently added preopens take precedence over less recently addded ones. + size_t match_len = 0; + int fd = -1; + for (size_t i = num_preopens; i > 0; --i) { + const preopen* pre = &preopens[i - 1]; + const char* prefix = pre->prefix; + size_t len = strlen(prefix); + + // If we haven't had a match yet, or the candidate path is longer than + // our current best match's path, and the candidate path is a prefix of + // the requested path, take that as the new best path. + if ((fd == -1 || len > match_len) && prefix_matches(prefix, len, path)) { + fd = pre->fd; + match_len = len; + } + } + + if (fd == -1) { + return false; + } + + // The relative path is the substring after the portion that was matched. + const char* computed = path + match_len; + + // Omit leading slashes in the relative path. + while (*computed == '/') + ++computed; + + // *at syscalls don't accept empty relative paths, so use "." instead. + if (*computed == '\0') + computed = "."; + + *need_unlock = false; + *resolved_dirfd = fd; + *path_ptr = computed; + return true; +} + +bool __paths_resolve_path(bool* need_unlock, + int* resolved_dirfd, + const char** path_ptr) { + // fast path + if (*resolved_dirfd != AT_FDCWD && (*path_ptr)[0] != '/') { + *need_unlock = false; + return true; + } + + LOCK(lock); + bool ret = resolve_path_unlocked(need_unlock, resolved_dirfd, path_ptr); + if (!*need_unlock) { + UNLOCK(lock); + } + return ret; +} + +void __paths_unlock() { UNLOCK(lock); } + +static void change_cwd_with_fd_unlocked(int newfd, + __wasi_device_t dev, + __wasi_inode_t ino) { + if (cwd_is_root) { + cwd_is_root = false; + } else { + if (!cwd_fd_from_preopen) { + __wasi_fd_close(cwd_fd); + } + } + + for (size_t i = 0; i < num_preopens; ++i) { + const preopen* pre = &preopens[i]; + + if (pre->dev == dev && pre->ino == ino) { + __wasi_fd_close(newfd); + + if (*pre->prefix == '\0') { + cwd_is_root = true; + } else { + cwd_fd = pre->fd; + cwd_fd_from_preopen = true; + } + + return; + } + } + + cwd_fd = newfd; + cwd_fd_from_preopen = false; +} + +static int change_cwd_unlocked(int dirfd, const char* path) { + __wasi_errno_t error; + + int newdir = openat(dirfd, path, O_DIRECTORY | O_SEARCH); + if (newdir == -1) { + error = errno; + goto out; + } + + __wasi_filestat_t sb; + error = __wasi_fd_filestat_get(newdir, &sb); + if (error != __WASI_ERRNO_SUCCESS) { + __wasi_fd_close(newdir); + goto out; + } + + change_cwd_with_fd_unlocked(newdir, sb.dev, sb.ino); + error = __WASI_ERRNO_SUCCESS; + +out: + return error; +} + +__wasi_errno_t __paths_chdir(const char* path) { + __wasi_errno_t error; + + LOCK(lock); + + int dirfd = AT_FDCWD; + + bool need_unlock; + bool ret = resolve_path_unlocked(&need_unlock, &dirfd, &path); + if (!ret) { + error = __WASI_ERRNO_NOENT; + goto out; + } + (void)need_unlock; + + error = change_cwd_unlocked(dirfd, path); + +out: + UNLOCK(lock); + return error; +} + +__wasi_errno_t __paths_fchdir(int fd) { + if (fd == AT_FDCWD) { + return EBADF; + } + + LOCK(lock); + __wasi_errno_t error = change_cwd_unlocked(fd, "."); + UNLOCK(lock); + return error; +} + +struct buf { + char* buf; + size_t size; + size_t capa; +}; + +static __wasi_errno_t buf_prepend(struct buf* buf, const char* str) { + size_t str_length = strlen(str); + size_t newsize = buf->size + str_length; + if (newsize > buf->capa) { + size_t newcapa = buf->capa != 0 ? buf->capa : 16; + while (newcapa < newsize) { + newcapa *= 2; + } + + char* newbuf = realloc(buf->buf, newcapa); + if (!newbuf) { + return errno; + } + buf->buf = newbuf; + buf->capa = newcapa; + } + + memmove(&buf->buf[str_length], &buf->buf[0], buf->size); + memcpy(&buf->buf[0], str, str_length); + buf->size = newsize; + + return __WASI_ERRNO_SUCCESS; +} + +static bool buf_empty(const struct buf* buf) { return buf->size == 0; } + +static __wasi_errno_t +calculate_cwd_path(char** out_buf, size_t* out_size, __wasi_fd_t fd) { + __wasi_errno_t error; + struct buf buf = {NULL, 0, 0}; + + int parent_for_search = -1; + + for (;;) { + __wasi_device_t dev; + __wasi_inode_t ino; + { + __wasi_filestat_t sb; + error = __wasi_fd_filestat_get(fd, &sb); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + dev = sb.dev; + ino = sb.ino; + } + + { + int new_parent = openat(fd, "..", O_DIRECTORY | O_SEARCH); + if (new_parent == -1) { + error = errno; + goto out; + } + + if (parent_for_search != -1) { + __wasi_fd_close(parent_for_search); + } + parent_for_search = new_parent; + } + + int parent = openat(parent_for_search, ".", O_DIRECTORY | O_RDONLY); + if (parent == -1) { + error = errno; + goto out; + } + + __wasi_device_t parent_dev; + __wasi_inode_t parent_ino; + { + __wasi_filestat_t sb; + error = __wasi_fd_filestat_get(parent, &sb); + if (error != __WASI_ERRNO_SUCCESS) { + __wasi_fd_close(parent); + goto out; + } + parent_dev = sb.dev; + parent_ino = sb.ino; + } + + if (parent_dev != dev) { + error = __WASI_ERRNO_NOENT; + __wasi_fd_close(parent); + goto out; + } + + DIR* parentdir = fdopendir(parent); + if (!parentdir) { + error = errno; + __wasi_fd_close(parent); + goto out; + } + + errno = 0; + struct dirent* dent; + bool found = false; + while ((dent = readdir(parentdir)) != NULL) { + if (dent->d_ino == ino) { + if (!buf_empty(&buf)) { + error = buf_prepend(&buf, "/"); + if (error != __WASI_ERRNO_SUCCESS) { + closedir(parentdir); + goto out; + } + } + + error = buf_prepend(&buf, dent->d_name); + if (error != __WASI_ERRNO_SUCCESS) { + closedir(parentdir); + goto out; + } + + found = true; + break; + } + } + error = errno; + closedir(parentdir); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + if (!found) { + error = __WASI_ERRNO_NOENT; + goto out; + } + + for (size_t i = 0; i < num_preopens; ++i) { + const preopen* pre = &preopens[i]; + if (parent_dev == pre->dev && parent_ino == pre->ino) { + // We have reached a root. + + error = buf_prepend(&buf, "/"); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + + error = buf_prepend(&buf, pre->prefix); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + + if (*pre->prefix != '\0') { + error = buf_prepend(&buf, "/"); + } + goto out; + } + } + + fd = parent_for_search; + } + +out: + if (parent_for_search != -1) { + __wasi_fd_close(parent_for_search); + } + if (error != __WASI_ERRNO_SUCCESS) { + free(buf.buf); + } else { + *out_buf = buf.buf; + *out_size = buf.size; + } + return error; +} + +__wasi_errno_t __paths_getcwd(char* buf, size_t* size) { + __wasi_errno_t error; + + LOCK(lock); + if (cwd_is_root) { + if (*size < 2) { + error = __WASI_ERRNO_RANGE; + goto out; + } + buf[0] = '/'; + buf[1] = '\0'; + *size = 2; + error = __WASI_ERRNO_SUCCESS; + goto out; + } + + if (cwd_fd_from_preopen) { + for (size_t i = 0; i < num_preopens; ++i) { + const preopen* pre = &preopens[i]; + if (pre->fd == cwd_fd) { + size_t cwd_len = strlen(pre->prefix); + if (1 + cwd_len + 1 > *size) { + error = __WASI_ERRNO_RANGE; + goto out; + } + + buf[0] = '/'; + strcpy(&buf[1], pre->prefix); + *size = 1 + cwd_len + 1; + + error = __WASI_ERRNO_SUCCESS; + goto out; + } + } + error = __WASI_ERRNO_NOENT; + assert(false); + } else { + char* cwd; + size_t cwd_size; + + error = calculate_cwd_path(&cwd, &cwd_size, cwd_fd); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + + if (cwd_size + 1 > *size) { + free(cwd); + error = __WASI_ERRNO_RANGE; + goto out; + } + + memcpy(buf, cwd, cwd_size); + free(cwd); + buf[cwd_size] = '\0'; + *size = cwd_size + 1; + + error = __WASI_ERRNO_SUCCESS; + } + +out: + UNLOCK(lock); + return error; +} + +#if defined(EMSCRIPTEN_PURE_WASI) + +static size_t preopen_capacity; + +#ifdef NDEBUG +#define assert_invariants() // assertions disabled +#else +static void assert_invariants(void) { + assert(num_preopens <= preopen_capacity); + assert(preopen_capacity == 0 || preopens != NULL); + assert(preopen_capacity == 0 || + preopen_capacity * sizeof(preopen) > preopen_capacity); + + for (size_t i = 0; i < num_preopens; ++i) { + const preopen* pre = &preopens[i]; + assert(pre->prefix != NULL); + assert(pre->fd != (__wasi_fd_t)-1); +#ifdef __wasm__ + assert((uintptr_t)pre->prefix < + (__uint128_t)__builtin_wasm_memory_size(0) * PAGESIZE); +#endif + } +} +#endif + +/// Allocate space for more preopens. Returns 0 on success and -1 on failure. +static bool resize_preopens(void) { + size_t start_capacity = 4; + size_t old_capacity = preopen_capacity; + size_t new_capacity = old_capacity == 0 ? start_capacity : old_capacity * 2; + + preopen* old_preopens = preopens; + preopen* new_preopens = calloc(sizeof(preopen), new_capacity); + if (new_preopens == NULL) { + return false; + } + + memcpy(new_preopens, old_preopens, num_preopens * sizeof(preopen)); + preopens = new_preopens; + preopen_capacity = new_capacity; + free(old_preopens); + + assert_invariants(); + return true; +} + +// Normalize an absolute path. Removes leading `/` and leading `./`, so the +// first character is the start of a directory name. This works because our +// process always starts with a working directory of `/`. Additionally translate +// `.` to the empty string. +static const char* strip_prefixes(const char* path) { + while (1) { + if (path[0] == '/') { + path++; + } else if (path[0] == '.' && path[1] == '/') { + path += 2; + } else if (path[0] == '.' && path[1] == 0) { + path++; + } else { + break; + } + } + + return path; +} + +/// Register the given preopened file descriptor under the given path. +/// +/// This function takes ownership of `prefix`. +static bool register_preopened_fd(__wasi_fd_t fd, + const char* relprefix, + __wasi_device_t dev, + __wasi_inode_t ino) { + // Check preconditions. + assert_invariants(); + assert(fd != AT_FDCWD); + assert(fd != -1); + assert(relprefix != NULL); + + if (num_preopens == preopen_capacity && !resize_preopens()) { + return false; + } + + char* prefix = strdup(strip_prefixes(relprefix)); + if (prefix == NULL) { + return false; + } + + preopens[num_preopens++] = (preopen){prefix, fd, dev, ino}; + + assert_invariants(); + return true; +} + +// Populate WASI preopens. +__attribute__((constructor(100))) // construct this before user code +static void _standalone_populate_preopens(void) { + // Skip stdin, stdout, and stderr, and count up until we reach an invalid + // file descriptor. + for (__wasi_fd_t fd = 3; fd != 0; ++fd) { + __wasi_prestat_t prestat; + __wasi_errno_t ret = __wasi_fd_prestat_get(fd, &prestat); + if (ret == __WASI_ERRNO_BADF) + break; + if (ret != __WASI_ERRNO_SUCCESS) + goto oserr; + switch (prestat.pr_type) { + case __WASI_PREOPENTYPE_DIR: { + char* prefix = malloc(prestat.u.dir.pr_name_len + 1); + if (prefix == NULL) + goto software; + + // TODO: Remove the cast on `path` once the witx is updated with + // char8 support. + ret = __wasi_fd_prestat_dir_name( + fd, (uint8_t*)prefix, prestat.u.dir.pr_name_len); + if (ret != __WASI_ERRNO_SUCCESS) + goto oserr; + prefix[prestat.u.dir.pr_name_len] = '\0'; + + __wasi_filestat_t fsb_cur; + ret = __wasi_path_filestat_get(fd, 0, ".", 1, &fsb_cur); + if (ret != __WASI_ERRNO_SUCCESS) + goto oserr; + + if (!register_preopened_fd(fd, prefix, fsb_cur.dev, fsb_cur.ino)) + goto software; + + free(prefix); + break; + } + default: + break; + } + } + + return; +oserr: + _Exit(EX_OSERR); +software: + _Exit(EX_SOFTWARE); +} + +#endif diff --git a/system/lib/standalone/paths.h b/system/lib/standalone/paths.h new file mode 100644 index 0000000000000..2a84dcf83cda5 --- /dev/null +++ b/system/lib/standalone/paths.h @@ -0,0 +1,46 @@ +#ifndef STANDALONE_PATHS_H +#define STANDALONE_PATHS_H + +#include +#include + +#include + +// +// Resolve a (dirfd, relative/absolute path) pair. +// +// Arguments: +// - `need_unlock`: Is set by the function. If `true`, must make sure to call +// `__paths_unlock()` when done with the `resolved_dirfd`. +// - `resolved_dirfd`: +// - as input: input dirfd, may be `AT_FDCWD` +// - as output: resolved dirfd, may be either a preopened fd or a fd +// representing the cwd. +// - `path_ptr`: +// - as input: pointer to a relative or absolute path +// - as output: a path relative to `resolved_dirfd` +// +// Returns: `true` if resolution was successful, `false` otherwise. +// +bool __paths_resolve_path(bool* need_unlock, + int* resolved_dirfd, + const char** path_ptr); + +// Must be called by the user when `need_unlock` of `__paths_resolve_path()` +// was true, and `resolved_dirfd` is no longer needed. +void __paths_unlock(); + +// Changes the current working directory to `path`, which may be either an +// absolute path or a path relative to the current working directory. +__wasi_errno_t __paths_chdir(const char* path); + +// Changes the current working directory to the directory represented by `fd`. +__wasi_errno_t __paths_fchdir(int fd); + +// Puts a string representing the current working directory into the buffer +// `buf`. `size` is an in/out parameter: input is the size of the buffer `buf`, +// output is the number of characters written into `buf`, including a +// terminating zero. +__wasi_errno_t __paths_getcwd(char* buf, size_t* size); + +#endif diff --git a/system/lib/standalone/standalone.c b/system/lib/standalone/standalone.c index 3a910ba662d62..e24e33cd190f5 100644 --- a/system/lib/standalone/standalone.c +++ b/system/lib/standalone/standalone.c @@ -7,15 +7,19 @@ #define _GNU_SOURCE #include +#include #include #include #include +#include #include #include +#include #include #include #include #include +#include #include #include @@ -23,8 +27,10 @@ #include #include +#include "../src/dirent/__dirent.h" #include "lock.h" #include "emscripten_internal.h" +#include "paths.h" /* * WASI support code. These are compiled with the program, and call out @@ -45,6 +51,62 @@ _Static_assert(CLOCK_MONOTONIC == __WASI_CLOCKID_MONOTONIC, "must match"); _Static_assert(CLOCK_PROCESS_CPUTIME_ID == __WASI_CLOCKID_PROCESS_CPUTIME_ID, "must match"); _Static_assert(CLOCK_THREAD_CPUTIME_ID == __WASI_CLOCKID_THREAD_CPUTIME_ID, "must match"); +static void wasi_filestat_to_stat(const __wasi_filestat_t* in, + struct stat* out) { + *out = (struct stat){ + .st_dev = in->dev, + .st_ino = in->ino, + .st_nlink = in->nlink, + .st_size = in->size, + .st_atim = __wasi_timestamp_to_timespec(in->atim), + .st_mtim = __wasi_timestamp_to_timespec(in->mtim), + .st_ctim = __wasi_timestamp_to_timespec(in->ctim), + }; + + // Convert file type to legacy types encoded in st_mode. + switch (in->filetype) { + case __WASI_FILETYPE_BLOCK_DEVICE: + out->st_mode |= S_IFBLK; + break; + case __WASI_FILETYPE_CHARACTER_DEVICE: + out->st_mode |= S_IFCHR; + break; + case __WASI_FILETYPE_DIRECTORY: + out->st_mode |= S_IFDIR; + break; + case __WASI_FILETYPE_REGULAR_FILE: + out->st_mode |= S_IFREG; + break; + case __WASI_FILETYPE_SOCKET_DGRAM: + case __WASI_FILETYPE_SOCKET_STREAM: + out->st_mode |= S_IFSOCK; + break; + case __WASI_FILETYPE_SYMBOLIC_LINK: + out->st_mode |= S_IFLNK; + break; + } +} + +static __wasi_fdflags_t fdflags_to_wasi_fdflags(int flags) { + __wasi_fdflags_t fs_flags = 0; + if (flags & O_APPEND) { + fs_flags |= __WASI_FDFLAGS_APPEND; + } + if (flags & O_DSYNC) { + fs_flags |= __WASI_FDFLAGS_DSYNC; + } + if (flags & O_NONBLOCK) { + fs_flags |= __WASI_FDFLAGS_NONBLOCK; + } + if (flags & O_RSYNC) { + fs_flags |= __WASI_FDFLAGS_RSYNC; + } + if (flags & O_SYNC) { + fs_flags |= __WASI_FDFLAGS_SYNC; + } + return fs_flags; +} + // mmap support is nonexistent. TODO: emulate simple mmaps using // stdio + malloc, which is slow but may help some things? @@ -65,21 +127,104 @@ weak int _munmap_js( return -ENOSYS; } -// open(), etc. - we just support the standard streams, with no -// corner case error checking; everything else is not permitted. -// TODO: full file support for WASI, or an option for it -// open() weak int __syscall_openat(int dirfd, intptr_t path, int flags, ...) { - if (!strcmp((const char*)path, "/dev/stdin")) { + const char* resolved_path = (const char*)path; + + if (!strcmp(resolved_path, "/dev/stdin")) { return STDIN_FILENO; } - if (!strcmp((const char*)path, "/dev/stdout")) { + if (!strcmp(resolved_path, "/dev/stdout")) { return STDOUT_FILENO; } - if (!strcmp((const char*)path, "/dev/stderr")) { + if (!strcmp(resolved_path, "/dev/stderr")) { return STDERR_FILENO; } - return -EPERM; + + bool unlock; + if (!__paths_resolve_path(&unlock, &dirfd, &resolved_path)) { + return -ENOENT; + } + + __wasi_errno_t error; + + // Compute rights corresponding with the access modes provided. + // Attempt to obtain all rights, except the ones that contradict the + // access mode provided to openat(). + __wasi_rights_t max = + ~(__WASI_RIGHTS_FD_DATASYNC | __WASI_RIGHTS_FD_READ | + __WASI_RIGHTS_FD_WRITE | __WASI_RIGHTS_FD_ALLOCATE | + __WASI_RIGHTS_FD_READDIR | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE); + { + int accmode = flags & O_ACCMODE; + if (accmode == O_RDONLY || accmode == O_RDWR || accmode == O_WRONLY) { + if (accmode == O_RDONLY || accmode == O_RDWR) { + max |= __WASI_RIGHTS_FD_READ | __WASI_RIGHTS_FD_READDIR; + } + if (accmode == O_WRONLY || accmode == O_RDWR) { + max |= __WASI_RIGHTS_FD_DATASYNC | __WASI_RIGHTS_FD_WRITE | + __WASI_RIGHTS_FD_ALLOCATE | + __WASI_RIGHTS_FD_FILESTAT_SET_SIZE; + } + } else if (accmode == O_EXEC || accmode == O_SEARCH) { + // Do nothing. + } else { + error = EINVAL; + goto out; + } + } + + // Ensure that we can actually obtain the minimal rights needed. + __wasi_fdstat_t fsb_cur; + error = __wasi_fd_fdstat_get(dirfd, &fsb_cur); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + + // Path lookup properties. + __wasi_lookupflags_t lookup_flags = 0; + if ((flags & O_NOFOLLOW) == 0) { + lookup_flags |= __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW; + } + + // Open file with appropriate rights. + __wasi_fdflags_t fs_flags = fdflags_to_wasi_fdflags(flags); + + __wasi_oflags_t oflags = 0; + if (flags & O_CREAT) { + oflags |= __WASI_OFLAGS_CREAT; + } + if (flags & O_DIRECTORY) { + oflags |= __WASI_OFLAGS_DIRECTORY; + } + if (flags & O_EXCL) { + oflags |= __WASI_OFLAGS_EXCL; + } + if (flags & O_TRUNC) { + oflags |= __WASI_OFLAGS_TRUNC; + } + + __wasi_rights_t fs_rights_base = max & fsb_cur.fs_rights_inheriting; + __wasi_rights_t fs_rights_inheriting = fsb_cur.fs_rights_inheriting; + __wasi_fd_t newfd; + + error = __wasi_path_open(dirfd, + lookup_flags, + resolved_path, + strlen(resolved_path), + oflags, + fs_rights_base, + fs_rights_inheriting, + fs_flags, + &newfd); + +out: + if (unlock) { + __paths_unlock(); + } + if (error != __WASI_ERRNO_SUCCESS) { + return -error; + } + return newfd; } weak int __syscall_ioctl(int fd, int op, ...) { @@ -87,15 +232,86 @@ weak int __syscall_ioctl(int fd, int op, ...) { } weak int __syscall_fcntl64(int fd, int cmd, ...) { - return -ENOSYS; + switch (cmd) { + case F_GETFD: + // Act as if the close-on-exec flag is always set. + return FD_CLOEXEC; + case F_SETFD: + // The close-on-exec flag is ignored. + return 0; + case F_GETFL: { + // Obtain the flags and the rights of the descriptor. + __wasi_fdstat_t fds; + __wasi_errno_t error = __wasi_fd_fdstat_get(fd, &fds); + if (error != 0) { + return -error; + } + + int oflags = 0; + if (fds.fs_flags & __WASI_FDFLAGS_APPEND) { + oflags |= O_APPEND; + } + if (fds.fs_flags & __WASI_FDFLAGS_DSYNC) { + oflags |= O_DSYNC; + } + if (fds.fs_flags & __WASI_FDFLAGS_NONBLOCK) { + oflags |= O_NONBLOCK; + } + if (fds.fs_flags & __WASI_FDFLAGS_RSYNC) { + oflags |= O_RSYNC; + } + if (fds.fs_flags & __WASI_FDFLAGS_SYNC) { + oflags |= O_SYNC; + } + + // Roughly approximate the access mode by converting the rights. + if ((fds.fs_rights_base & + (__WASI_RIGHTS_FD_READ | __WASI_RIGHTS_FD_READDIR)) != 0) { + if ((fds.fs_rights_base & __WASI_RIGHTS_FD_WRITE) != 0) + oflags |= O_RDWR; + else + oflags |= O_RDONLY; + } else if ((fds.fs_rights_base & __WASI_RIGHTS_FD_WRITE) != 0) { + oflags |= O_WRONLY; + } else { + _Static_assert(O_SEARCH == O_EXEC, ""); + oflags |= O_SEARCH; + } + return oflags; + } + case F_SETFL: { + // Set new file descriptor flags. + va_list ap; + va_start(ap, cmd); + int flags = va_arg(ap, int); + va_end(ap); + + __wasi_fdflags_t fs_flags = fdflags_to_wasi_fdflags(flags); + + __wasi_errno_t error = __wasi_fd_fdstat_set_flags(fd, fs_flags); + if (error != 0) { + return -error; + } + return 0; + } + default: + return -EINVAL; + } } weak int __syscall_fstat64(int fd, intptr_t buf) { - return -ENOSYS; + __wasi_filestat_t sb; + __wasi_errno_t error = __wasi_fd_filestat_get(fd, &sb); + if (error != __WASI_ERRNO_SUCCESS) { + return -error; + } + + wasi_filestat_to_stat(&sb, (struct stat*)buf); + return 0; } weak int __syscall_stat64(intptr_t path, intptr_t buf) { - return -ENOSYS; + return __syscall_newfstatat(AT_FDCWD, path, buf, 0); } weak int __syscall_dup(int fd) { @@ -103,15 +319,280 @@ weak int __syscall_dup(int fd) { } weak int __syscall_mkdirat(int dirfd, intptr_t path, int mode) { - return -ENOSYS; + const char* resolved_path = (const char*)path; + + bool unlock; + if (!__paths_resolve_path(&unlock, &dirfd, &resolved_path)) { + return -ENOENT; + } + + __wasi_errno_t error = + __wasi_path_create_directory(dirfd, resolved_path, strlen(resolved_path)); + + if (unlock) { + __paths_unlock(); + } + if (error != __WASI_ERRNO_SUCCESS) { + return -error; + } + return 0; } weak int __syscall_newfstatat(int dirfd, intptr_t path, intptr_t buf, int flags) { - return -ENOSYS; + // Convert flags to WASI. + __wasi_lookupflags_t lookup_flags = 0; + if ((flags & AT_SYMLINK_NOFOLLOW) == 0) { + lookup_flags |= __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW; + } + + const char* resolved_path = (const char*)path; + + bool unlock; + if (!__paths_resolve_path(&unlock, &dirfd, &resolved_path)) { + return -ENOENT; + } + + __wasi_filestat_t fsb_cur; + __wasi_errno_t error = __wasi_path_filestat_get( + dirfd, lookup_flags, resolved_path, strlen(resolved_path), &fsb_cur); + + if (unlock) { + __paths_unlock(); + } + if (error != __WASI_ERRNO_SUCCESS) { + return -error; + } + + wasi_filestat_to_stat(&fsb_cur, (struct stat*)buf); + return 0; } weak int __syscall_lstat64(intptr_t path, intptr_t buf) { - return -ENOSYS; + return __syscall_newfstatat(AT_FDCWD, path, buf, AT_SYMLINK_NOFOLLOW); +} + +weak int __syscall_symlinkat(intptr_t target_arg, int newdirfd, intptr_t linkpath) { + const char* resolved_linkpath = (const char*)linkpath; + + bool unlock; + if (!__paths_resolve_path(&unlock, &newdirfd, &resolved_linkpath)) { + return -ENOENT; + } + + const char* target = (const char*)target_arg; + + __wasi_errno_t error = __wasi_path_symlink(target, + strlen(target), + newdirfd, + resolved_linkpath, + strlen(resolved_linkpath)); + + if (unlock) { + __paths_unlock(); + } + if (error != __WASI_ERRNO_SUCCESS) { + return -error; + } + return 0; +} + +weak int __syscall_chdir(intptr_t path) { + __wasi_errno_t error = __paths_chdir((const char*)path); + if (error != 0) { + return -error; + } + + return 0; +} + +weak int __syscall_fchdir(int fd) { + __wasi_errno_t error = __paths_fchdir(fd); + if (error != 0) { + return -error; + } + + return 0; +} + +weak int __syscall_getcwd(intptr_t buf, size_t size) { + __wasi_errno_t error = __paths_getcwd((char*)buf, &size); + if (error != 0) { + return -error; + } + + return (int)size; +} + +weak int __syscall_getdents64(int fd, intptr_t dirp, size_t count) { + __wasi_errno_t error; + intptr_t dirpointer = dirp; + struct dirent *de = (void *)dirpointer; + + // Check if the result buffer is too small. + if (count / sizeof(struct dirent) == 0) { + return -EINVAL; + } + + __wasi_dirent_t entry; + + // Create new buffer size to save same amount of __wasi_dirent_t as dirp records. + size_t buffer_size = (count / sizeof(struct dirent)) * (sizeof(entry) + 256); + char *buffer = malloc(buffer_size); + if (buffer == NULL) { + return -errno; + } + + size_t buffer_processed = buffer_size; + size_t buffer_used = buffer_size; + size_t dirent_processed = 0; + + // We assume `dirp` always points to the buffer of a `DIR*`. + __wasi_dircookie_t cookie = + ((DIR*)((char*)dirpointer - offsetof(DIR, buf)))->tell; + + for (;;) { + // Extract the next dirent header. + size_t buffer_left = buffer_used - buffer_processed; + if (buffer_left < sizeof(__wasi_dirent_t)) { + // End-of-file. + if (buffer_used < buffer_size) { + break; + } + + goto read_entries; + } + __wasi_dirent_t entry; + memcpy(&entry, buffer + buffer_processed, sizeof(entry)); + + size_t entry_size = sizeof(__wasi_dirent_t) + entry.d_namlen; + if (entry.d_namlen == 0) { + // Invalid pathname length. Skip the entry. + buffer_processed += entry_size; + continue; + } + + // The entire entry must be present in buffer space. If not, read + // the entry another time. Ensure that the read buffer is large + // enough to fit at least this single entry. + if (buffer_left < entry_size) { + while (buffer_size < entry_size) { + buffer_size *= 2; + } + char *new_buffer = realloc(buffer, buffer_size); + if (new_buffer == NULL) { + error = errno; + goto out; + } + buffer = new_buffer; + goto read_entries; + } + + const char *name = buffer + buffer_processed + sizeof(entry); + buffer_processed += entry_size; + + // Skip entries that do not fit in the dirent name buffer. + if (entry.d_namlen > sizeof de->d_name) { + continue; + } + + // Skip entries having null bytes in the filename. + if (memchr(name, '\0', entry.d_namlen) != NULL) { + continue; + } + + off_t d_ino = entry.d_ino; + unsigned char d_type = entry.d_type; + + // Adapted from wasi-libc. + if (d_ino == 0 && (entry.d_namlen != 2 || memcmp(name, "..", 2) != 0)) { + __wasi_filestat_t sb; + error = __wasi_path_filestat_get(fd, 0, name, entry.d_namlen, &sb); + if (error == __WASI_ERRNO_NOENT) { + // The file disappeared before we could read it, so skip it. + continue; + } + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + + // Fill in the inode. + d_ino = sb.ino; + + // In case someone raced with us and replaced the object with this name + // with another of a different type, update the type too. + d_type = sb.filetype; + } + + de->d_ino = d_ino; + + // Map the right WASI type to dirent type. + // I could not get the dirent.h import to work to use defines. + switch (d_type) { + case __WASI_FILETYPE_UNKNOWN: + de->d_type = 0; + break; + case __WASI_FILETYPE_BLOCK_DEVICE: + de->d_type = 6; + break; + case __WASI_FILETYPE_CHARACTER_DEVICE: + de->d_type = 2; + break; + case __WASI_FILETYPE_DIRECTORY: + de->d_type = 4; + break; + case __WASI_FILETYPE_REGULAR_FILE: + de->d_type = 8; + break; + case __WASI_FILETYPE_SOCKET_DGRAM: + de->d_type = 12; + break; + case __WASI_FILETYPE_SOCKET_STREAM: + de->d_type = 12; + break; + case __WASI_FILETYPE_SYMBOLIC_LINK: + de->d_type = 10; + break; + default: + de->d_type = 0; + break; + } + + de->d_off = entry.d_next; + de->d_reclen = sizeof(struct dirent); + memcpy(de->d_name, name, entry.d_namlen); + de->d_name[entry.d_namlen] = '\0'; + cookie = entry.d_next; + dirent_processed = dirent_processed + sizeof(struct dirent); + + // Can't fit more in my buffer. + if (dirent_processed + sizeof(struct dirent) > count) { + break; + } + + // Set entry to next entry in memory. + dirpointer = dirpointer + sizeof(struct dirent); + de = (void*)(dirpointer); + + continue; + + read_entries: + // Load more directory entries and continue. + error = __wasi_fd_readdir( + fd, (uint8_t*)buffer, buffer_size, cookie, &buffer_used); + if (error != __WASI_ERRNO_SUCCESS) { + goto out; + } + buffer_processed = 0; + } + + error = __WASI_ERRNO_SUCCESS; + +out: + if (error != __WASI_ERRNO_SUCCESS) { + free(buffer); + return -error; + } + return dirent_processed; } // Emscripten additions diff --git a/test/common.py b/test/common.py index beb266d2d0021..847c26df0257a 100644 --- a/test/common.py +++ b/test/common.py @@ -614,7 +614,7 @@ def can_do_standalone(self, impure=False): # Impure means a test that cannot run in a wasm VM yet, as it is not 100% # standalone. We can still run them with the JS code though. -def also_with_standalone_wasm(impure=False): +def also_with_standalone_wasm(impure=False, exclude_engines=None): def decorated(func): @wraps(func) def metafunc(self, standalone): @@ -623,6 +623,9 @@ def metafunc(self, standalone): if not standalone: func(self) else: + nonlocal exclude_engines + if exclude_engines is None: + exclude_engines = [] if not can_do_standalone(self, impure): self.skipTest('Test configuration is not compatible with STANDALONE_WASM') self.set_setting('STANDALONE_WASM') @@ -635,9 +638,19 @@ def metafunc(self, standalone): self.emcc_args.append('-Wno-unused-command-line-argument') # if we are impure, disallow all wasm engines if impure: - self.wasm_engines = [] - nodejs = self.require_node() - self.node_args += shared.node_bigint_flags(nodejs) + wasm_engines = [] + else: + wasm_engines = [engine for engine in self.wasm_engines + if all(excluded not in os.path.basename(engine[0]) for excluded in exclude_engines)] + if 'node' in exclude_engines: + self.js_engines = [] + if not wasm_engines: + self.skipTest('no WASM engines available for this test') + else: + nodejs = self.require_node() + self.node_args += shared.node_bigint_flags(nodejs) + # `self.require_node()` clears `self.wasm_engines`, so set them here. + self.wasm_engines = wasm_engines func(self) parameterize(metafunc, {'': (False,), @@ -1505,7 +1518,8 @@ def cleanup(line): def run_js(self, filename, engine=None, args=None, assert_returncode=0, interleaved_output=True, - input=None): + input=None, + cwd=None): # use files, as PIPE can get too full and hang us stdout_file = self.in_dir('stdout') stderr_file = None @@ -1529,6 +1543,7 @@ def run_js(self, filename, engine=None, args=None, jsrun.run_js(filename, engine, args, stdout=stdout, stderr=stderr, + cwd=cwd, assert_returncode=assert_returncode, input=input) except subprocess.TimeoutExpired as e: @@ -1957,7 +1972,7 @@ def _build_and_run(self, filename, expected_output, args=None, js_file = self.build(filename, **kwargs) self.assertExists(js_file) - engines = self.js_engines.copy() + engines = [(el, None) for el in self.js_engines.copy()] if len(engines) > 1 and not self.use_all_engines: engines = engines[:1] # In standalone mode, also add wasm vms as we should be able to run there too. @@ -1966,11 +1981,15 @@ def _build_and_run(self, filename, expected_output, args=None, # like with js engines, but for now as we bring it up, test in all of them if not self.wasm_engines: logger.warning('no wasm engine was found to run the standalone part of this test') - engines += self.wasm_engines + engines += [(engine, os.path.basename(engine[0])) for engine in self.wasm_engines] if len(engines) == 0: self.fail('No JS engine present to run this test with. Check %s and the paths therein.' % config.EM_CONFIG) - for engine in engines: + for engine, subdir in engines: + if subdir: + subdir = self.in_dir(subdir) + ensure_dir(subdir) js_output = self.run_js(js_file, engine, args, + cwd=subdir, assert_returncode=assert_returncode, interleaved_output=interleaved_output) js_output = js_output.replace('\r\n', '\n') diff --git a/test/dirent/test_readdir.c b/test/dirent/test_readdir.c index 8a8428a63cc11..6ac3a27c37417 100644 --- a/test/dirent/test_readdir.c +++ b/test/dirent/test_readdir.c @@ -30,9 +30,12 @@ static void create_file(const char *path, const char *buffer, int mode) { void setup() { int err; + // Make this test not rely on `chdir` for standalone WASM mode. +#if !defined(STANDALONE_WASM) err = mkdir("testtmp", 0777); // can't call it tmp, that already exists CHECK(!err); chdir("testtmp"); +#endif err = mkdir("nocanread", 0111); CHECK(!err); err = mkdir("foobar", 0777); @@ -53,8 +56,9 @@ void test() { dir = opendir("noexist"); assert(!dir); assert(errno == ENOENT); -// NODERAWFS tests run as root, and the root user can opendir any directory -#ifndef NODERAWFS + // NODERAWFS/STANDALONE_WASM tests run as root, and the root user can opendir + // any directory +#if !defined(NODERAWFS) && !defined(STANDALONE_WASM) dir = opendir("nocanread"); assert(!dir); assert(errno == EACCES); diff --git a/test/fcntl/test_fcntl.c b/test/fcntl/test_fcntl.c index 3839f7ef95783..ad9d20491de94 100644 --- a/test/fcntl/test_fcntl.c +++ b/test/fcntl/test_fcntl.c @@ -47,21 +47,6 @@ int main() { printf("\n"); errno = 0; - printf("F_GETFL: %d\n", !!(fcntl(f, F_GETFL) & O_RDWR)); - printf("errno: %d\n", errno); - printf("\n"); - errno = 0; - - printf("F_SETFL: %d\n", fcntl(f, F_SETFL, O_APPEND)); - printf("errno: %d\n", errno); - printf("\n"); - errno = 0; - - printf("F_GETFL/2: %d\n", !!(fcntl(f, F_GETFL) & (O_RDWR | O_APPEND))); - printf("errno: %d\n", errno); - printf("\n"); - errno = 0; - struct flock lk; lk.l_type = 42; printf("F_GETLK: %d\n", fcntl(f, F_GETLK, &lk)); diff --git a/test/fcntl/test_fcntl.out b/test/fcntl/test_fcntl.out index 6192b8558e68b..f43965771cab6 100644 --- a/test/fcntl/test_fcntl.out +++ b/test/fcntl/test_fcntl.out @@ -19,15 +19,6 @@ errno: 0 F_SETFD: 0 errno: 0 -F_GETFL: 1 -errno: 0 - -F_SETFL: 0 -errno: 0 - -F_GETFL/2: 1 -errno: 0 - F_GETLK: 0 errno: 0 lk.l_type == F_UNLCK: 1 diff --git a/test/fcntl/test_fcntl_fl.c b/test/fcntl/test_fcntl_fl.c new file mode 100644 index 0000000000000..5e62dc6fe1561 --- /dev/null +++ b/test/fcntl/test_fcntl_fl.c @@ -0,0 +1,43 @@ +/* + * Copyright 2011 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. + */ + +#include +#include +#include +#include + +int main() { + int f = open("test", + O_RDWR +#if defined(STANDALONE_WASM) + | O_CREAT +#endif + , + 0777); +#if defined(STANDALONE_WASM) + assert(f >= 0); +#else + assert(f == 3); +#endif + + printf("F_GETFL: %d\n", !!(fcntl(f, F_GETFL) & O_RDWR)); + printf("errno: %d\n", errno); + printf("\n"); + errno = 0; + + printf("F_SETFL: %d\n", fcntl(f, F_SETFL, O_APPEND)); + printf("errno: %d\n", errno); + printf("\n"); + errno = 0; + + printf("F_GETFL/2: %d\n", !!(fcntl(f, F_GETFL) & (O_RDWR | O_APPEND))); + printf("errno: %d\n", errno); + printf("\n"); + errno = 0; + + return 0; +} diff --git a/test/fcntl/test_fcntl_fl.out b/test/fcntl/test_fcntl_fl.out new file mode 100644 index 0000000000000..468006a4e9f37 --- /dev/null +++ b/test/fcntl/test_fcntl_fl.out @@ -0,0 +1,8 @@ +F_GETFL: 1 +errno: 0 + +F_SETFL: 0 +errno: 0 + +F_GETFL/2: 1 +errno: 0 diff --git a/test/jsrun.py b/test/jsrun.py index d7a9cf817922e..e08d7e2219038 100644 --- a/test/jsrun.py +++ b/test/jsrun.py @@ -40,10 +40,15 @@ def make_command(filename, engine, args=None): is_jsc = 'jsc' in jsengine or 'javascriptcore' in jsengine is_wasmer = 'wasmer' in jsengine is_wasmtime = 'wasmtime' in jsengine + is_toywasm = 'toywasm' in jsengine command_flags = [] if is_wasmer: command_flags += ['run'] - if is_wasmer or is_wasmtime: + elif is_wasmtime: + command_flags += ['--dir', '.', '--'] + elif is_toywasm: + command_flags += ['--wasi', '--wasi-dir', '.', '--'] + if is_wasmer or is_wasmtime or is_toywasm: # in a wasm runtime, run the wasm, not the js filename = shared.replace_suffix(filename, '.wasm') # Separates engine flags from script flags diff --git a/test/stat/test_fstatat.c b/test/stat/test_fstatat.c index 9103f8db8ea51..987237d0dfee4 100644 --- a/test/stat/test_fstatat.c +++ b/test/stat/test_fstatat.c @@ -56,7 +56,7 @@ void test() { assert(s.st_rdev == 0); assert(s.st_size); assert(s.st_ctime); -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) && !defined(STANDALONE_WASM) assert(s.st_blksize == 4096); // WasmFS correctly counts 512B blocks, but MEMFS counts 4kb blocks. #ifdef WASMFS @@ -77,7 +77,7 @@ void test() { assert(s.st_rdev == 0); assert(s.st_size == 6); assert(s.st_ctime); -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) && !defined(STANDALONE_WASM) assert(s.st_blksize == 4096); assert(s.st_blocks == 1); #endif @@ -113,7 +113,7 @@ void test() { assert(s.st_rdev == 0); assert(s.st_size); assert(s.st_ctime); -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) && !defined(STANDALONE_WASM) assert(s.st_blksize == 4096); #ifdef WASMFS assert(s.st_blocks == 8); @@ -138,7 +138,7 @@ void test() { assert(s.st_rdev == 0); assert(s.st_size == 6); assert(s.st_ctime); -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) && !defined(STANDALONE_WASM) assert(s.st_blksize == 4096); assert(s.st_blocks == 1); #endif @@ -157,7 +157,7 @@ void test() { assert(s.st_rdev == 0); assert(s.st_size == 4); assert(s.st_ctime); -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) && !defined(STANDALONE_WASM) assert(s.st_blksize == 4096); assert(s.st_blocks == 1); #endif diff --git a/test/test_core.py b/test/test_core.py index 074285c6b62b7..a37679d6f9b48 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5532,12 +5532,15 @@ def test_fileno(self): self.do_run(src, '3\n') @also_with_noderawfs + @also_with_standalone_wasm(exclude_engines=['node', 'wasmer']) def test_readdir(self): if self.get_setting('WASMFS') and self.get_setting('NODERAWFS'): # WasmFS + NODERAWFS lacks ino numbers in directory listings, see # https://github.com/emscripten-core/emscripten/issues/19418 # We need to tell the test we are in this mode so it can ignore them. self.emcc_args += ['-DWASMFS_NODERAWFS'] + if self.get_setting('STANDALONE_WASM'): + self.emcc_args += ['-DSTANDALONE_WASM'] self.do_run_in_out_file_test('dirent/test_readdir.c') @also_with_wasm_bigint @@ -5556,7 +5559,10 @@ def test_statx(self): self.set_setting("FORCE_FILESYSTEM") self.do_runf('stat/test_statx.c', 'success') + @also_with_standalone_wasm(exclude_engines=['node', 'wasmer']) def test_fstatat(self): + if self.get_setting('STANDALONE_WASM'): + self.emcc_args += ['-DSTANDALONE_WASM'] self.do_runf('stat/test_fstatat.c', 'success') @crossplatform @@ -5580,8 +5586,18 @@ def test_fcntl(self): self.add_pre_run("FS.createDataFile('/', 'test', 'abcdef', true, true, false);") self.do_run_in_out_file_test('fcntl/test_fcntl.c') + @also_with_wasmfs + @also_with_standalone_wasm(exclude_engines=['node', 'toywasm']) + def test_fcntl_fl(self): + if self.get_setting('STANDALONE_WASM'): + self.emcc_args += ['-DSTANDALONE_WASM'] + else: + self.add_pre_run("FS.createDataFile('/', 'test', 'abcdef', true, true, false);") + self.do_run_in_out_file_test('fcntl/test_fcntl_fl.c') + @crossplatform @also_with_nodefs_both + @also_with_standalone_wasm(exclude_engines=['node', 'wasmer']) def test_fcntl_open(self): nodefs = '-DNODEFS' in self.emcc_args or '-DNODERAWFS' in self.emcc_args if nodefs and WINDOWS: @@ -5951,9 +5967,12 @@ def test_unistd_access(self): out_suffix = '' self.do_run_in_out_file_test('unistd/access.c', out_suffix=out_suffix) + @also_with_standalone_wasm(exclude_engines=['node', 'wasmtime', 'wasmer']) def test_unistd_curdir(self): if self.get_setting('WASMFS'): self.set_setting('FORCE_FILESYSTEM') + if self.get_setting('STANDALONE_WASM'): + self.emcc_args += ['-DSTANDALONE_WASM'] self.do_run_in_out_file_test('unistd/curdir.c') @also_with_noderawfs diff --git a/test/test_other.py b/test/test_other.py index d411e7d7cd355..a8c8ca5cc21c1 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -6455,7 +6455,8 @@ def test_force_stdlibs(self): # See https://github.com/emscripten-core/emscripten/issues/22161 self.do_runf('hello_world.c', emcc_args=['-sWASM_BIGINT']) - @also_with_standalone_wasm() + # `wasmtime` hangs + @also_with_standalone_wasm(exclude_engines=['wasmtime']) def test_time(self): self.do_other_test('test_time.c') @@ -14295,7 +14296,10 @@ def test_unistd_mkdir(self): self.do_run_in_out_file_test('wasmfs/wasmfs_mkdir.c') @also_with_wasmfs + @also_with_standalone_wasm(exclude_engines=['node', 'wasmtime', 'wasmer']) def test_unistd_cwd(self): + if self.get_setting('STANDALONE_WASM'): + self.emcc_args += ['-DSTANDALONE_WASM'] self.do_run_in_out_file_test('wasmfs/wasmfs_chdir.c') def test_unistd_chown(self): @@ -15410,7 +15414,8 @@ def test_standalone_whole_archive(self): def test_proxy_to_worker(self, args): self.do_runf('hello_world.c', emcc_args=['--proxy-to-worker'] + args) - @also_with_standalone_wasm() + # functions from `emscripten/console.h` only work with node + @also_with_standalone_wasm(impure=True) def test_console_out(self): self.do_other_test('test_console_out.c', regex=True) diff --git a/test/unistd/curdir.c b/test/unistd/curdir.c index 81b7dc2b47e1f..4cc8cff33ef11 100644 --- a/test/unistd/curdir.c +++ b/test/unistd/curdir.c @@ -5,6 +5,8 @@ * found in the LICENSE file. */ +#include + #include #include #include @@ -14,6 +16,7 @@ #include int main() { +#if !defined(STANDALONE_WASM) EM_ASM( var dummy_device = FS.makedev(64, 0); FS.registerDevice(dummy_device, {}); @@ -23,6 +26,26 @@ int main() { FS.symlink('/folder', '/link'); FS.writeFile('/file', "", { mode: 0o777 }); ); +#else + { + int fd = open("device", O_CREAT, 0777); + assert(fd >= 0); + close(fd); + } + { + int rv = mkdir("folder", 0777); + assert(rv == 0); + } + { + int rv = symlink("folder", "link"); + assert(rv == 0); + } + { + int fd = open("file", O_CREAT, 0777); + assert(fd >= 0); + close(fd); + } +#endif char buffer[256]; diff --git a/test/wasmfs/wasmfs_chdir.c b/test/wasmfs/wasmfs_chdir.c index e1d3d7efe1fa9..7653f18ba8011 100644 --- a/test/wasmfs/wasmfs_chdir.c +++ b/test/wasmfs/wasmfs_chdir.c @@ -22,6 +22,15 @@ int main() { assert(mkdir("working", 0777) != -1); assert(mkdir("/working/test", 0777) != -1); +#ifdef STANDALONE_WASM + assert(mkdir("/dev", 0777) != -1); + { + int fd = open("/dev/file", O_CREAT, 0777); + assert(fd >= 0); + close(fd); + } +#endif + // Try to pass a size of 0. errno = 0; getcwd(cwd, 0); @@ -92,7 +101,11 @@ int main() { printf("Current working dir: %s\n", cwd); // Try to change cwd to a file. +#ifdef STANDALONE_WASM + chdir("/dev/file"); +#else chdir("/dev/stdout"); +#endif printf("Errno: %s\n", strerror(errno)); assert(errno == ENOTDIR); ret = getcwd(cwd, sizeof(cwd)); diff --git a/tools/building.py b/tools/building.py index ab33a8602afa9..a5e31bd32cd2b 100644 --- a/tools/building.py +++ b/tools/building.py @@ -819,6 +819,10 @@ def metadce(js_file, wasm_file, debug_info, last): 'clock_time_get', 'path_open', 'random_get', + 'path_filestat_get', + 'path_create_directory', + 'path_symlink', + 'fd_readdir', } for item in graph: if 'import' in item and item['import'][1] in WASI_IMPORTS: diff --git a/tools/maint/gen_sig_info.py b/tools/maint/gen_sig_info.py index d8890da052749..8fc51b55f8467 100755 --- a/tools/maint/gen_sig_info.py +++ b/tools/maint/gen_sig_info.py @@ -162,6 +162,10 @@ 'args_get', 'args_sizes_get', 'random_get', + 'path_filestat_get', + 'path_create_directory', + 'path_symlink', + 'fd_readdir', } diff --git a/tools/system_libs.py b/tools/system_libs.py index d82ab077bb3a2..6faaccf01de92 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -2217,6 +2217,7 @@ def get_files(self): path='system/lib/standalone', filenames=['standalone.c', 'standalone_wasm_stdio.c', + 'paths.c', '__main_void.c']) # It is more efficient to use JS methods for time, normally. files += files_in_path(