/** * @license * Copyright 2018 The Emscripten Authors * SPDX-License-Identifier: MIT */ addToLibrary({ $NODERAWFS__deps: ['$ERRNO_CODES', '$FS', '$NODEFS', '$mmapAlloc', '$FS_modeStringToFlags', '$NODERAWFS_stream_funcs'], $NODERAWFS__postset: ` if (!ENVIRONMENT_IS_NODE) { throw new Error("NODERAWFS is currently only supported on Node.js environment.") } var nodeTTY = require('node:tty'); function _wrapNodeError(func) { return (...args) => { try { return func(...args) } catch (e) { // Hack for Deno which throws BadResource instead of EBADF: // https://github.com/emscripten-core/emscripten/issues/26239 if (e.name == 'BadResource') { e.code = 'EBADF'; } if (e.code) { throw new FS.ErrnoError(ERRNO_CODES[e.code]); } throw e; } } } function _wrapNodeStreamFunc(func, vfs_func) { return _wrapNodeError((stream, ...args) => { if (stream.stream_ops) { // this stream was created by some other FS. e.g: PIPEFS. return vfs_func(stream, ...args); } return func(stream, ...args); }); } // Use this to reference our in-memory filesystem /** @suppress {partialAlias} */ var VFS = {...FS}; // Wrap the whole in-memory filesystem API with // our Node.js based functions for (const [key, value] of Object.entries(NODERAWFS)) { FS[key] = _wrapNodeError(value); } for (const [key, value] of Object.entries(NODERAWFS_stream_funcs)) { FS[key] = _wrapNodeStreamFunc(value, FS[key]); }`, $NODERAWFS: { lookup(parent, name) { #if ASSERTIONS assert(parent) assert(parent.path) #endif return FS.lookupPath(`${parent.path}/${name}`).node; }, lookupPath(path, opts = {}) { if (opts.parent) { path = PATH.dirname(path); } var st = fs.lstatSync(path); var mode = NODEFS.getMode(path); return { path, node: { id: st.ino, mode, node_ops: NODERAWFS, path }}; }, createStandardStreams() { FS.createStream({ nfd: 0, position: 0, path: '/dev/stdin', flags: 0, seekable: false }, 0); var paths = [,'/dev/stdout', '/dev/stderr']; for (var i = 1; i < 3; i++) { FS.createStream({ nfd: i, position: 0, path: paths[i], flags: {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}}, seekable: false }, i); } }, // generic function for all node creation cwd() { return process.cwd(); }, chdir(...args) { process.chdir(...args); }, mknod(path, mode) { if (FS.isDir(path)) { fs.mkdirSync(path, mode); } else { fs.writeFileSync(path, '', { mode: mode }); } }, mkdir(...args) { fs.mkdirSync(...args); }, symlink(...args) { fs.symlinkSync(...args); }, rename(...args) { fs.renameSync(...args); }, rmdir(...args) { fs.rmdirSync(...args); }, readdir(...args) { return ['.', '..'].concat(fs.readdirSync(...args)); }, unlink(...args) { fs.unlinkSync(...args); }, readlink(...args) { return fs.readlinkSync(...args); }, stat(path, dontFollow) { var stat = dontFollow ? fs.lstatSync(path) : fs.statSync(path); if (NODEFS.isWindows) { // Windows does not report the 'x' permission bit, so propagate read // bits to execute bits. stat.mode |= (stat.mode & {{{ cDefs.S_IRUGO }}}) >> 2; } return stat; }, fstat(fd) { var stream = FS.getStreamChecked(fd); return fs.fstatSync(stream.nfd); }, statfs(path) { // Node's fs.statfsSync API doesn't provide these attributes so include // some defaults. var defaults = { fsid: 42, flags: 2, namelen: 255, } return Object.assign(defaults, fs.statfsSync(path)); }, statfsStream(stream) { return FS.statfs(stream.path); }, chmod(path, mode, dontFollow) { mode &= {{{ cDefs.S_IALLUGO }}}; if (NODEFS.isWindows) { // Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR) // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod mode &= {{{ cDefs.S_IRUSR | cDefs.S_IWUSR }}}; } if (dontFollow && fs.lstatSync(path).isSymbolicLink()) { // Node (and indeed linux) does not support chmod on symlinks // https://nodejs.org/api/fs.html#fslchmodsyncpath-mode throw new FS.ErrnoError({{{ cDefs.EOPNOTSUPP }}}); } fs.chmodSync(path, mode); }, fchmod(fd, mode) { var stream = FS.getStreamChecked(fd); fs.fchmodSync(stream.nfd, mode); }, chown(...args) { fs.chownSync(...args); }, fchown(fd, owner, group) { var stream = FS.getStreamChecked(fd); fs.fchownSync(stream.nfd, owner, group); }, truncate(path, len) { // See https://github.com/nodejs/node/issues/35632 if (len < 0) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); } return fs.truncateSync(path, len); }, ftruncate(fd, len) { // See https://github.com/nodejs/node/issues/35632 if (len < 0) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); } var stream = FS.getStreamChecked(fd); fs.ftruncateSync(stream.nfd, len); }, utime(path, atime, mtime) { // null here for atime or mtime means UTIME_OMIT was passed. Since node // doesn't support this concept we need to first find the existing // timestamps in order to preserve them. if ((atime === null) || (mtime === null)) { var st = fs.statSync(path); atime ||= st.atimeMs; mtime ||= st.mtimeMs; } fs.utimesSync(path, atime/1000, mtime/1000); }, open(path, flags, mode) { flags = FS_modeStringToFlags(flags); var pathTruncated = path.split('/').map((s) => s.slice(0, 255)).join('/'); var nfd = fs.openSync(pathTruncated, NODEFS.flagsForNode(flags), mode); var st = fs.fstatSync(nfd); if (flags & {{{ cDefs.O_DIRECTORY }}} && !st.isDirectory()) { fs.closeSync(nfd); throw new FS.ErrnoError(ERRNO_CODES.ENOTDIR); } var newMode = NODEFS.getMode(pathTruncated); var node = { id: st.ino, mode: newMode, node_ops: NODERAWFS, path } return FS.createStream({ nfd, position: 0, path, flags, node, seekable: true }); }, createStream(stream, fd) { // Call the original FS.createStream var rtn = VFS.createStream(stream, fd); // Detect PIPEFS streams and skip the refcnt/tty initialization in that case. if (!stream.stream_ops) { rtn.shared.refcnt ??= 0; rtn.shared.refcnt++; rtn.tty = nodeTTY.isatty(rtn.nfd); } return rtn; }, }, /** * These functions all take a stream as the first argument which * could either be a stream created by NODERAWFS itself, or, for example * one created by PIPEFS. We wrap all these function in an extra check. */ $NODERAWFS_stream_funcs: { close(stream) { VFS.closeStream(stream.fd); // Don't close stdin/stdout/stderr since they are used by node itself. if (--stream.shared.refcnt <= 0 && stream.nfd > 2) { // This stream is created by our Node.js filesystem, close the // native file descriptor when its reference count drops to 0. fs.closeSync(stream.nfd); } }, llseek(stream, offset, whence) { var position = offset; if (whence === {{{ cDefs.SEEK_CUR }}}) { position += stream.position; } else if (whence === {{{ cDefs.SEEK_END }}}) { position += fs.fstatSync(stream.nfd).size; } else if (whence !== {{{ cDefs.SEEK_SET }}}) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); } if (position < 0) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); } stream.position = position; return position; }, read(stream, buffer, offset, length, position) { var seeking = typeof position != 'undefined'; if (!seeking && stream.seekable) position = stream.position; var bytesRead = fs.readSync(stream.nfd, buffer, offset, length, position); // update position marker when non-seeking if (!seeking) stream.position += bytesRead; return bytesRead; }, write(stream, buffer, offset, length, position) { if (stream.flags & {{{ cDefs.O_APPEND }}}) { // seek to the end before writing in append mode FS.llseek(stream, 0, {{{ cDefs.SEEK_END }}}); } var seeking = typeof position != 'undefined'; if (!seeking && stream.seekable) position = stream.position; var bytesWritten = fs.writeSync(stream.nfd, buffer, offset, length, position); // update position marker when non-seeking if (!seeking) stream.position += bytesWritten; return bytesWritten; }, mmap(stream, length, position, prot, flags) { if (!length) { throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); } var ptr = mmapAlloc(length); FS.read(stream, HEAP8, ptr, length, position); return { ptr, allocated: true }; }, msync(stream, buffer, offset, length, mmapFlags) { FS.write(stream, buffer, 0, length, offset); // should we check if bytesWritten and length are the same? return 0; }, ioctl(stream, cmd, arg) { throw new FS.ErrnoError({{{ cDefs.ENOTTY }}}); }, }, });