| /** | |
| * @license | |
| * Copyright 2018 The Emscripten Authors | |
| * SPDX-License-Identifier: MIT | |
| */ | |
| addToLibrary({ | |
| $NODERAWFS__deps: ['$ERRNO_CODES', '$FS', '$NODEFS', '$TTY', '$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() { | |
| var paths = ['/dev/stdin', '/dev/stdout', '/dev/stderr']; | |
| for (var i = 0; i < 3; i++) { | |
| // Like open() below, give the stream a node carrying the identity of | |
| // whatever the inherited descriptor refers to (tty, pipe, file, ...): | |
| // every stream has a node. | |
| var st = fs.fstatSync(i); | |
| var node = { id: st.ino, mode: st.mode, node_ops: NODERAWFS, path: paths[i] }; | |
| var flags = i ? {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}} : 0; | |
| FS.createStream({ nfd: i, position: 0, path: paths[i], flags, node }, 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); }, | |
| link(oldpath, newpath, flags) { | |
| // AT_SYMLINK_FOLLOW (0x400): dereference oldpath if it is a symlink, | |
| // since node's link(2) links to the symlink itself by default. | |
| if (flags & 0x400) { | |
| oldpath = fs.realpathSync(oldpath); | |
| } | |
| fs.linkSync(oldpath, newpath); | |
| }, | |
| 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 = false) { | |
| 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); | |
| // Virtual streams (pipes, sockets) have no backing node fd; defer to their | |
| // own getattr rather than node's fs.fstatSync. | |
| var getattr = stream.stream_ops?.getattr ?? stream.node.node_ops?.getattr; | |
| if (getattr) { | |
| return getattr(stream.stream_ops?.getattr ? stream : stream.node); | |
| } | |
| 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 = false) { | |
| 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, dontFollow = false) { | |
| // 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 = dontFollow ? fs.lstatSync(path) : fs.statSync(path); | |
| atime ??= st.atimeMs; | |
| mtime ??= st.mtimeMs; | |
| } | |
| if (dontFollow) { | |
| fs.lutimesSync(path, atime/1000, mtime/1000); | |
| } else { | |
| 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++; | |
| if (nodeTTY.isatty(rtn.nfd)) { | |
| rtn.tty = { ops: TTY.default_tty_ops }; | |
| rtn.seekable = false; | |
| } | |
| } | |
| 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 }}}); | |
| }, | |
| }, | |
| }); |