diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index bb990b3f9c142..7182a4733b380 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -411,6 +411,7 @@ sigs = { _wasmfs_node_insert_directory__sig: 'ipi', _wasmfs_node_insert_file__sig: 'ipi', _wasmfs_node_open__sig: 'ipp', + _wasmfs_node_path_get_root__sig: 'ippi', _wasmfs_node_read__sig: 'iipiip', _wasmfs_node_readdir__sig: 'ipp', _wasmfs_node_readlink__sig: 'ippi', diff --git a/src/lib/libwasmfs_node.js b/src/lib/libwasmfs_node.js index bf749467ffdc6..475c0e4880674 100644 --- a/src/lib/libwasmfs_node.js +++ b/src/lib/libwasmfs_node.js @@ -5,7 +5,28 @@ */ addToLibrary({ - $wasmfsNodeIsWindows: !!process.platform.match(/^win/), +#if !ENVIRONMENT_MAY_BE_NODE + _wasmfs_node_readdir: (path_p, vec) => {}, + _wasmfs_node_get_mode: (path_p, mode_p) => {}, + _wasmfs_node_stat_size: (path_p, size_p) => {}, + _wasmfs_node_fstat_size: (fd, size_p) => {}, + _wasmfs_node_insert_file: (path_p, mode) => {}, + _wasmfs_node_insert_directory: (path_p, mode) => {}, + _wasmfs_node_unlink: (path_p) => {}, + _wasmfs_node_rmdir: (path_p) => {}, + _wasmfs_node_truncate: (path_p, len) => {}, + _wasmfs_node_ftruncate: (fd, len) => {}, + _wasmfs_node_open: (path_p, mode_p) => {}, + _wasmfs_node_rename: (from_path_p, to_path_p) => {}, + _wasmfs_node_symlink: (target_path_p, linkpath_path_p) => {}, + _wasmfs_node_readlink: (path_p, target_p, bufsize) => {}, + _wasmfs_node_close: (fd) => {}, + _wasmfs_node_read: (fd, buf_p, len, pos, nread_p) => {}, + _wasmfs_node_write: (fd, buf_p, len, pos, nwritten_p) => {}, + _wasmfs_node_path_get_root: (path_p, target_p, bufsize) => {}, +#else + $wasmfsNodeIsWindows: "!!globalThis.process?.platform.match(/^win/)", + $nodePath: "ENVIRONMENT_IS_NODE ? require('node:path') : undefined", $wasmfsNodeConvertNodeCode__deps: ['$ERRNO_CODES'], $wasmfsNodeConvertNodeCode: (e) => { @@ -31,7 +52,7 @@ addToLibrary({ if (wasmfsNodeIsWindows) { // Node.js on Windows never represents permission bit 'x', so // propagate read bits to execute bits - stat.mode |= (stat.mode & {{{ cDefs.S_IRUSR | cDefs.S_IRGRP | cDefs.S_IROTH }}}) >> 2; + stat.mode |= (stat.mode & {{{ cDefs.S_IRUGO }}}) >> 2; } return stat; }, @@ -157,15 +178,15 @@ addToLibrary({ }, _wasmfs_node_truncate__i53abi: true, - _wasmfs_node_truncate__deps : ['$wasmfsTry'], - _wasmfs_node_truncate : (path_p, len) => { + _wasmfs_node_truncate__deps: ['$wasmfsTry'], + _wasmfs_node_truncate: (path_p, len) => { if (isNaN(len)) return -{{{ cDefs.EOVERFLOW }}}; return wasmfsTry(() => fs.truncateSync(UTF8ToString(path_p), len)); }, _wasmfs_node_ftruncate__i53abi: true, - _wasmfs_node_ftruncate__deps : ['$wasmfsTry'], - _wasmfs_node_ftruncate : (fd, len) => { + _wasmfs_node_ftruncate__deps: ['$wasmfsTry'], + _wasmfs_node_ftruncate: (fd, len) => { if (isNaN(len)) return -{{{ cDefs.EOVERFLOW }}}; return wasmfsTry(() => fs.ftruncateSync(fd, len)); }, @@ -193,7 +214,7 @@ addToLibrary({ }); }, - _wasmfs_node_close__deps: [], + _wasmfs_node_close__deps: ['$wasmfsTry'], _wasmfs_node_close: (fd) => { return wasmfsTry(() => { fs.closeSync(fd); @@ -206,21 +227,26 @@ addToLibrary({ return wasmfsTry(() => { // TODO: Cache open file descriptors to guarantee that opened files will // still exist when we try to access them. - let nread = fs.readSync(fd, new Int8Array(HEAPU8.buffer, buf_p, len), 0, len, pos); + let nread = fs.readSync(fd, HEAPU8, buf_p, len, pos); {{{ makeSetValue('nread_p', 0, 'nread', 'i32') }}}; // implicitly return 0 }); }, - _wasmfs_node_write__deps : ['$wasmfsTry'], - _wasmfs_node_write : (fd, buf_p, len, pos, nwritten_p) => { + _wasmfs_node_write__deps: ['$wasmfsTry'], + _wasmfs_node_write: (fd, buf_p, len, pos, nwritten_p) => { return wasmfsTry(() => { // TODO: Cache open file descriptors to guarantee that opened files will // still exist when we try to access them. - let nwritten = fs.writeSync(fd, new Int8Array(HEAPU8.buffer, buf_p, len), 0, len, pos); + let nwritten = fs.writeSync(fd, HEAPU8, buf_p, len, pos); {{{ makeSetValue('nwritten_p', 0, 'nwritten', 'i32') }}}; // implicitly return 0 }); }, + _wasmfs_node_path_get_root__deps: ['$nodePath'], + _wasmfs_node_path_get_root: (path_p, target_p, bufsize) => { + return stringToUTF8(nodePath.parse(UTF8ToString(path_p)).root, target_p, bufsize); + }, +#endif }); diff --git a/system/include/emscripten/wasmfs.h b/system/include/emscripten/wasmfs.h index 024d0119ad09e..744458661f416 100644 --- a/system/include/emscripten/wasmfs.h +++ b/system/include/emscripten/wasmfs.h @@ -77,7 +77,7 @@ backend_t wasmfs_create_memory_backend(void); // backend_t wasmfs_create_fetch_backend(const char* _Nonnull base_url, uint32_t chunk_size); -backend_t wasmfs_create_node_backend(const char* _Nonnull root); +backend_t wasmfs_create_node_backend(const char* _Nonnull mount_path); // Note: this cannot be called on the browser main thread because it might // deadlock while waiting for the OPFS dedicated worker thread to be spawned. @@ -111,6 +111,11 @@ void wasmfs_flush(void); // default backend is used. backend_t wasmfs_create_root_dir(void); +// A hook users can do to create the working directory. Overriding this allows +// the user to set a particular backend as the CWD. If this is not set then the +// root_backend is used. +backend_t wasmfs_create_working_dir(backend_t root_backend); + // A hook users can do to run code during WasmFS startup. This hook happens // before file preloading, so user code could create backends and mount them, // which would then affect in which backend the preloaded files are loaded (the diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index d8e584c01136a..18a9c305d322d 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,6 +21,10 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; + virtual std::string getRootPath(std::string_view path) { + return path.front() == '/' ? "/" : ""; + } + virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index a3a7ad19b32f3..24e78bc63d6a7 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -191,7 +191,9 @@ class NodeDirectory : public Directory { if (_wasmfs_node_get_mode(childPath.c_str(), &mode)) { return nullptr; } - if (S_ISREG(mode)) { + // Allow reading from character device files too (e.g. `/dev/random`, + // `/dev/urandom`) + if (S_ISREG(mode) || S_ISCHR(mode)) { return std::make_shared(mode, getBackend(), childPath); } else if (S_ISDIR(mode)) { return std::make_shared(mode, getBackend(), childPath); @@ -294,14 +296,23 @@ class NodeBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } + + std::string getRootPath(std::string_view path) override { + char buf[PATH_MAX]; + auto pathStr = std::string(path); + if (_wasmfs_node_path_get_root(pathStr.c_str(), buf, PATH_MAX) < 0) { + WASMFS_UNREACHABLE("getRootPath cannot fail"); + } + return std::string(buf); + } }; // TODO: symlink extern "C" { -backend_t wasmfs_create_node_backend(const char* root) { - return wasmFS.addBackend(std::make_unique(root)); +backend_t wasmfs_create_node_backend(const char* mount_path) { + return wasmFS.addBackend(std::make_unique(mount_path)); } void EMSCRIPTEN_KEEPALIVE _wasmfs_node_record_dirent( diff --git a/system/lib/wasmfs/backends/node_backend.h b/system/lib/wasmfs/backends/node_backend.h index 7e6e496b7c73f..f8318c940d16d 100644 --- a/system/lib/wasmfs/backends/node_backend.h +++ b/system/lib/wasmfs/backends/node_backend.h @@ -52,4 +52,6 @@ int _wasmfs_node_read( int _wasmfs_node_write( int fd, const void* buf, uint32_t len, uint32_t pos, uint32_t* nwritten); +int _wasmfs_node_path_get_root(const char* path, const char *buf, int bufsize); + } // extern "C" diff --git a/system/lib/wasmfs/backends/noderawfs_root.cpp b/system/lib/wasmfs/backends/noderawfs_root.cpp index 6ed7af81daf56..32788b60ba908 100644 --- a/system/lib/wasmfs/backends/noderawfs_root.cpp +++ b/system/lib/wasmfs/backends/noderawfs_root.cpp @@ -6,5 +6,9 @@ #include "emscripten/wasmfs.h" backend_t wasmfs_create_root_dir(void) { + return wasmfs_create_node_backend(""); +} + +backend_t wasmfs_create_working_dir(backend_t root_backend) { return wasmfs_create_node_backend("."); } diff --git a/system/lib/wasmfs/paths.cpp b/system/lib/wasmfs/paths.cpp index 3d1fbf4c081e8..8262d12b3003a 100644 --- a/system/lib/wasmfs/paths.cpp +++ b/system/lib/wasmfs/paths.cpp @@ -69,10 +69,12 @@ ParsedParent doParseParent(std::string_view path, return {-ENOENT}; } + auto rootPath = curr->getBackend()->getRootPath(path); + // Handle absolute paths. - if (path.front() == '/') { + if (!rootPath.empty()) { curr = wasmFS.getRootDirectory(); - path.remove_prefix(1); + path.remove_prefix(rootPath.size()); } // Ignore trailing '/'. diff --git a/system/lib/wasmfs/wasmfs.cpp b/system/lib/wasmfs/wasmfs.cpp index 7a771a8ff4bd0..b4fb7b0d563d4 100644 --- a/system/lib/wasmfs/wasmfs.cpp +++ b/system/lib/wasmfs/wasmfs.cpp @@ -37,7 +37,8 @@ __attribute__((init_priority(100))) WasmFS wasmFS; __attribute__((weak)) extern "C" void wasmfs_before_preload(void) {} // Set up global data structures and preload files. -WasmFS::WasmFS() : rootDirectory(initRootDirectory()), cwd(rootDirectory) { +WasmFS::WasmFS() + : rootDirectory(initRootDirectory()), cwd(initCWD(rootDirectory)) { wasmfs_before_preload(); preloadFiles(); } @@ -87,8 +88,8 @@ WasmFS::~WasmFS() { rootDirectory->locked().setParent(nullptr); } -// Special backends that want to install themselves as the root use this hook. -// Otherwise, we use the default backends. +// Hooks for backends that want to install themselves as the root or CWD. +// Otherwise, we use the default memory backends. __attribute__((weak)) extern "C" backend_t wasmfs_create_root_dir(void) { #ifdef WASMFS_CASE_INSENSITIVE return createIgnoreCaseBackend([]() { return createMemoryBackend(); }); @@ -97,6 +98,11 @@ __attribute__((weak)) extern "C" backend_t wasmfs_create_root_dir(void) { #endif } +__attribute__((weak)) extern "C" backend_t +wasmfs_create_working_dir(backend_t root_backend) { + return root_backend; +} + std::shared_ptr WasmFS::initRootDirectory() { auto rootBackend = wasmfs_create_root_dir(); auto rootDirectory = @@ -126,6 +132,23 @@ std::shared_ptr WasmFS::initRootDirectory() { return rootDirectory; } +std::shared_ptr +WasmFS::initCWD(const std::shared_ptr& root) { + auto rootBackend = root->getBackend(); + auto cwdBackend = wasmfs_create_working_dir(rootBackend); + if (rootBackend == cwdBackend) { + return root; + } + + auto cwd = cwdBackend->createDirectory(S_IRUGO | S_IXUGO | S_IWUGO); + auto lockedCwd = cwd->locked(); + + // The root directory is the parent of the CWD. + lockedCwd.setParent(root); + + return cwd; +} + // Initialize files specified by the --preload-file option. // Set up directories and files in wasmFS$preloadedDirs and // wasmFS$preloadedFiles from JS. This function will be called before any file diff --git a/system/lib/wasmfs/wasmfs.h b/system/lib/wasmfs/wasmfs.h index 682d53064ee77..d369239a769c0 100644 --- a/system/lib/wasmfs/wasmfs.h +++ b/system/lib/wasmfs/wasmfs.h @@ -32,6 +32,9 @@ class WasmFS { // dev/stderr. Refers to the same std streams in the open file table. std::shared_ptr initRootDirectory(); + // Private method to initialize CWD once. + std::shared_ptr initCWD(const std::shared_ptr& root); + // Initialize files specified by --preload-file option. void preloadFiles(); diff --git a/test/fs/test_fs_dev_random.c b/test/fs/test_fs_dev_random.c index 4200f0bdc8139..a88a04fc75619 100644 --- a/test/fs/test_fs_dev_random.c +++ b/test/fs/test_fs_dev_random.c @@ -13,12 +13,14 @@ int main() { int nread; fp = fopen("/dev/random", "r"); - nread = fread(&data, 1, byte_count, fp); + assert(fp != NULL); + nread = fread(data, 1, byte_count, fp); assert(nread == byte_count); fclose(fp); fp = fopen("/dev/urandom", "r"); - nread = fread(&data, 1, byte_count, fp); + assert(fp != NULL); + nread = fread(data, 1, byte_count, fp); assert(nread == byte_count); fclose(fp); diff --git a/test/test_core.py b/test/test_core.py index dfdbdb1eba9ed..e190eb54ce570 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -6149,9 +6149,12 @@ def test_unistd_unlink(self): # Several differences/bugs on non-linux including https://github.com/nodejs/node/issues/18014 # TODO: NODERAWFS in WasmFS - if '-DNODERAWFS' in self.cflags and os.geteuid() == 0: + if '-DNODERAWFS' in self.cflags: # 0 if root user - self.cflags += ['-DSKIP_ACCESS_TESTS'] + if os.geteuid() == 0: + self.cflags += ['-DSKIP_ACCESS_TESTS'] + if self.get_setting('WASMFS'): + self.skipTest('https://github.com/emscripten-core/emscripten/issues/18112') self.do_runf('unistd/unlink.c', 'success') diff --git a/test/test_other.py b/test/test_other.py index 936dff4783e1c..5c58c4afe378e 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -9255,16 +9255,68 @@ def test_noderawfs_disables_embedding(self): self.assert_fail(base + ['--preload-file', 'somefile'], expected) self.assert_fail(base + ['--embed-file', 'somefile'], expected) + @crossplatform + @also_with_wasmfs def test_noderawfs_access_abspath(self): create_file('foo', 'bar') create_file('access.c', r''' + #include + #include #include int main(int argc, char** argv) { - return access(argv[1], F_OK); + printf("testing access to %s\n", argv[1]); + int rtn = access(argv[1], F_OK); + assert(rtn == 0); + return 0; } ''') self.do_runf('access.c', cflags=['-sNODERAWFS'], args=[os.path.abspath('foo')]) + @crossplatform + @also_with_wasmfs + def test_noderawfs_open_abspath(self): + create_file('foo', 'bar') + create_file('open.c', r''' + #include + #include + #include + #include + int main(int argc, char** argv) { + printf("testing open to %s\n", argv[1]); + int fd = open(argv[1], O_RDONLY, 0644); + assert(fd >= 0); + int rtn = close(fd); + assert(rtn == 0); + return 0; + } + ''') + self.do_runf('open.c', cflags=['-sNODERAWFS'], args=[os.path.abspath('foo')]) + + def test_wasmfs_nodefs_stubs(self): + # This is essentially equivalent to building with `-sWASMFS -sNODERAWFS`, except that + # the Wasm binary can also be used on the web. Ensure that this use case is supported. + create_file('main.c', r''' + #include + #include + + EM_JS(bool, is_node, (), { return ENVIRONMENT_IS_NODE; }); + + backend_t wasmfs_create_root_dir() { + return is_node() ? wasmfs_create_node_backend("") + : wasmfs_create_memory_backend(); + } + + backend_t wasmfs_create_working_dir(backend_t root_backend) { + return is_node() ? wasmfs_create_node_backend(".") + : root_backend; + } + + int main(int argc, char** argv) { + return 0; + } + ''') + self.run_process([EMCC, 'main.c', '-sWASMFS', '-sENVIRONMENT=web']) + def test_noderawfs_readfile_prerun(self): create_file('foo', 'bar') self.add_pre_run("console.log(FS.readFile('foo', { encoding: 'utf8' }));") @@ -13210,11 +13262,10 @@ def test_unistd_chown(self): self.set_setting('WASMFS') self.do_run_in_out_file_test('wasmfs/wasmfs_chown.c') - @wasmfs_all_backends def test_wasmfs_getdents(self): # Run only in WASMFS for now. self.set_setting('FORCE_FILESYSTEM') - self.do_run_in_out_file_test('wasmfs/wasmfs_getdents.c') + self.do_run_in_out_file_test('wasmfs/wasmfs_getdents.c', cflags=['-sWASMFS']) def test_wasmfs_jsfile(self): self.set_setting('WASMFS') @@ -13801,15 +13852,13 @@ def test_fs_icase(self): @crossplatform @with_all_fs def test_std_filesystem(self): - if self.get_setting('NODERAWFS') and self.get_setting('WASMFS'): - self.skipTest('https://github.com/emscripten-core/emscripten/issues/24830') + if (WINDOWS or MACOS) and self.get_setting('NODERAWFS') and self.get_setting('WASMFS'): + self.skipTest('fails with ENOTEMPTY (Directory not empty) during fs::remove_all') self.do_other_test('test_std_filesystem.cpp') @crossplatform @with_all_fs def test_std_filesystem_tempdir(self): - if self.get_setting('NODERAWFS') and self.get_setting('WASMFS'): - self.skipTest('https://github.com/emscripten-core/emscripten/issues/24830') self.do_other_test('test_std_filesystem_tempdir.cpp', cflags=['-g']) def test_strict_js_closure(self):