| /** |
| * @license |
| * Copyright 2013 The Emscripten Authors |
| * SPDX-License-Identifier: MIT |
| */ |
| |
| addToLibrary({ |
| $MEMFS__deps: ['$FS', '$mmapAlloc'], |
| $MEMFS: { |
| ops_table: null, |
| mount(mount) { |
| return MEMFS.createNode(null, '/', {{{ cDefs.S_IFDIR | 0o777 }}}, 0); |
| }, |
| createNode(parent, name, mode, dev) { |
| if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { |
| // not supported |
| throw new FS.ErrnoError({{{ cDefs.EPERM }}}); |
| } |
| MEMFS.ops_table ||= { |
| dir: { |
| node: { |
| getattr: MEMFS.node_ops.getattr, |
| setattr: MEMFS.node_ops.setattr, |
| lookup: MEMFS.node_ops.lookup, |
| mknod: MEMFS.node_ops.mknod, |
| rename: MEMFS.node_ops.rename, |
| unlink: MEMFS.node_ops.unlink, |
| rmdir: MEMFS.node_ops.rmdir, |
| readdir: MEMFS.node_ops.readdir, |
| symlink: MEMFS.node_ops.symlink |
| }, |
| stream: { |
| llseek: MEMFS.stream_ops.llseek |
| } |
| }, |
| file: { |
| node: { |
| getattr: MEMFS.node_ops.getattr, |
| setattr: MEMFS.node_ops.setattr |
| }, |
| stream: { |
| llseek: MEMFS.stream_ops.llseek, |
| read: MEMFS.stream_ops.read, |
| write: MEMFS.stream_ops.write, |
| mmap: MEMFS.stream_ops.mmap, |
| msync: MEMFS.stream_ops.msync |
| } |
| }, |
| link: { |
| node: { |
| getattr: MEMFS.node_ops.getattr, |
| setattr: MEMFS.node_ops.setattr, |
| readlink: MEMFS.node_ops.readlink |
| }, |
| stream: {} |
| }, |
| chrdev: { |
| node: { |
| getattr: MEMFS.node_ops.getattr, |
| setattr: MEMFS.node_ops.setattr |
| }, |
| stream: FS.chrdev_stream_ops |
| } |
| }; |
| var node = FS.createNode(parent, name, mode, dev); |
| if (FS.isDir(node.mode)) { |
| node.node_ops = MEMFS.ops_table.dir.node; |
| node.stream_ops = MEMFS.ops_table.dir.stream; |
| node.contents = {}; |
| } else if (FS.isFile(node.mode)) { |
| node.node_ops = MEMFS.ops_table.file.node; |
| node.stream_ops = MEMFS.ops_table.file.stream; |
| // The actual number of bytes used in the typed array, as opposed to |
| // contents.length which gives the whole capacity. |
| node.usedBytes = 0; |
| // The byte data of the file is stored in a typed array. |
| // Note: typed arrays are not resizable like normal JS arrays are, so |
| // there is a small penalty involved for appending file writes that |
| // continuously grow a file similar to std::vector capacity vs used. |
| node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); |
| } else if (FS.isLink(node.mode)) { |
| node.node_ops = MEMFS.ops_table.link.node; |
| node.stream_ops = MEMFS.ops_table.link.stream; |
| } else if (FS.isChrdev(node.mode)) { |
| node.node_ops = MEMFS.ops_table.chrdev.node; |
| node.stream_ops = MEMFS.ops_table.chrdev.stream; |
| } |
| node.atime = node.mtime = node.ctime = Date.now(); |
| // add the new node to the parent |
| if (parent) { |
| parent.contents[name] = node; |
| parent.atime = parent.mtime = parent.ctime = node.atime; |
| } |
| return node; |
| }, |
| |
| // Given a file node, returns its file data converted to a typed array. |
| getFileDataAsTypedArray(node) { |
| #if ASSERTIONS |
| assert(FS.isFile(node.mode), 'getFileDataAsTypedArray called on non-file'); |
| #endif |
| return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. |
| }, |
| |
| // Allocates a new backing store for the given node so that it can fit at |
| // least newSize amount of bytes. |
| // May allocate more, to provide automatic geometric increase and amortized |
| // linear performance appending writes. |
| // Never shrinks the storage. |
| expandFileStorage(node, newCapacity) { |
| var prevCapacity = node.contents.length; |
| if (prevCapacity >= newCapacity) return; // No need to expand, the storage was already large enough. |
| // Don't expand strictly to the given requested limit if it's only a very |
| // small increase, but instead geometrically grow capacity. |
| // For small filesizes (<1MB), perform size*2 geometric increase, but for |
| // large sizes, do a much more conservative size*1.125 increase to avoid |
| // overshooting the allocation cap by a very large margin. |
| var CAPACITY_DOUBLING_MAX = 1024 * 1024; |
| newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) >>> 0); |
| if (prevCapacity) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. |
| var oldContents = MEMFS.getFileDataAsTypedArray(node); |
| node.contents = new Uint8Array(newCapacity); // Allocate new storage. |
| node.contents.set(oldContents); |
| }, |
| |
| // Performs an exact resize of the backing file storage to the given size, |
| // if the size is not exactly this, the storage is fully reallocated. |
| resizeFileStorage(node, newSize) { |
| if (node.usedBytes == newSize) return; |
| var oldContents = node.contents; |
| node.contents = new Uint8Array(newSize); // Allocate new storage. |
| node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. |
| node.usedBytes = newSize; |
| }, |
| |
| node_ops: { |
| getattr(node) { |
| var attr = {}; |
| // device numbers reuse inode numbers. |
| attr.dev = FS.isChrdev(node.mode) ? node.id : 1; |
| attr.ino = node.id; |
| attr.mode = node.mode; |
| attr.nlink = 1; |
| attr.uid = 0; |
| attr.gid = 0; |
| attr.rdev = node.rdev; |
| if (FS.isDir(node.mode)) { |
| attr.size = 4096; |
| } else if (FS.isFile(node.mode)) { |
| attr.size = node.usedBytes; |
| } else if (FS.isLink(node.mode)) { |
| attr.size = node.link.length; |
| } else { |
| attr.size = 0; |
| } |
| attr.atime = new Date(node.atime); |
| attr.mtime = new Date(node.mtime); |
| attr.ctime = new Date(node.ctime); |
| // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), |
| // but this is not required by the standard. |
| attr.blksize = 4096; |
| attr.blocks = Math.ceil(attr.size / attr.blksize); |
| return attr; |
| }, |
| setattr(node, attr) { |
| for (const key of ["mode", "atime", "mtime", "ctime"]) { |
| if (attr[key] != null) { |
| node[key] = attr[key]; |
| } |
| } |
| if (attr.size !== undefined) { |
| MEMFS.resizeFileStorage(node, attr.size); |
| } |
| }, |
| lookup(parent, name) { |
| #if ASSERTIONS |
| throw new FS.ErrnoError({{{ cDefs.ENOENT }}}); |
| #else |
| // This error may happen quite a bit. To avoid overhead we reuse it (and |
| // suffer a lack of stack info). |
| if (!MEMFS.doesNotExistError) { |
| MEMFS.doesNotExistError = new FS.ErrnoError({{{ cDefs.ENOENT }}}); |
| /** @suppress {checkTypes} */ |
| MEMFS.doesNotExistError.stack = '<generic error, no stack>'; |
| } |
| throw MEMFS.doesNotExistError; |
| #endif |
| }, |
| mknod(parent, name, mode, dev) { |
| return MEMFS.createNode(parent, name, mode, dev); |
| }, |
| rename(old_node, new_dir, new_name) { |
| var new_node; |
| try { |
| new_node = FS.lookupNode(new_dir, new_name); |
| } catch (e) {} |
| if (new_node) { |
| if (FS.isDir(old_node.mode)) { |
| // if we're overwriting a directory at new_name, make sure it's empty. |
| for (var i in new_node.contents) { |
| throw new FS.ErrnoError({{{ cDefs.ENOTEMPTY }}}); |
| } |
| } |
| FS.hashRemoveNode(new_node); |
| } |
| // do the internal rewiring |
| delete old_node.parent.contents[old_node.name]; |
| new_dir.contents[new_name] = old_node; |
| old_node.name = new_name; |
| new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); |
| }, |
| unlink(parent, name) { |
| delete parent.contents[name]; |
| parent.ctime = parent.mtime = Date.now(); |
| }, |
| rmdir(parent, name) { |
| var node = FS.lookupNode(parent, name); |
| for (var i in node.contents) { |
| throw new FS.ErrnoError({{{ cDefs.ENOTEMPTY }}}); |
| } |
| delete parent.contents[name]; |
| parent.ctime = parent.mtime = Date.now(); |
| }, |
| readdir(node) { |
| return ['.', '..', ...Object.keys(node.contents)]; |
| }, |
| symlink(parent, newname, oldpath) { |
| var node = MEMFS.createNode(parent, newname, 0o777 | {{{ cDefs.S_IFLNK }}}, 0); |
| node.link = oldpath; |
| return node; |
| }, |
| readlink(node) { |
| if (!FS.isLink(node.mode)) { |
| throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); |
| } |
| return node.link; |
| }, |
| }, |
| stream_ops: { |
| read(stream, buffer, offset, length, position) { |
| var contents = stream.node.contents; |
| if (position >= stream.node.usedBytes) return 0; |
| var size = Math.min(stream.node.usedBytes - position, length); |
| #if ASSERTIONS |
| assert(size >= 0); |
| #endif |
| buffer.set(contents.subarray(position, position + size), offset); |
| return size; |
| }, |
| |
| /** |
| * Writes the byte range (buffer[offset], buffer[offset+length]) to offset |
| * 'position' into the file pointed by 'stream'. |
| * @param {TypedArray} buffer |
| * @param {boolean=} canOwn - A boolean that tells if this function can |
| * take ownership of the passed in buffer from the subbuffer portion |
| * that the typed array view 'buffer' points to. The underlying |
| * ArrayBuffer can be larger than that, but canOwn=true will not take |
| * ownership of the portion outside the bytes addressed by the view. |
| * This means that with canOwn=true, creating a copy of the bytes is |
| * avoided, but the caller shouldn't touch the passed in range of |
| * bytes anymore since their contents now represent file data inside |
| * the filesystem. |
| */ |
| write(stream, buffer, offset, length, position, canOwn) { |
| #if ASSERTIONS |
| assert(buffer.subarray, 'FS.write expects a TypedArray'); |
| #endif |
| #if ALLOW_MEMORY_GROWTH |
| // If the buffer is located in main memory (HEAP), and if |
| // memory can grow, we can't hold on to references of the |
| // memory buffer, as they may get invalidated. That means we |
| // need to copy its contents. |
| if (buffer.buffer === HEAP8.buffer) { |
| canOwn = false; |
| } |
| #endif // ALLOW_MEMORY_GROWTH |
| |
| if (!length) return 0; |
| var node = stream.node; |
| node.mtime = node.ctime = Date.now(); |
| |
| if (canOwn) { |
| #if ASSERTIONS |
| assert(position === 0, 'canOwn must imply no weird position inside the file'); |
| #endif |
| node.contents = buffer.subarray(offset, offset + length); |
| node.usedBytes = length; |
| } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. |
| node.contents = buffer.slice(offset, offset + length); |
| node.usedBytes = length; |
| } else { |
| MEMFS.expandFileStorage(node, position+length); |
| // Use typed array write which is available. |
| node.contents.set(buffer.subarray(offset, offset + length), position); |
| node.usedBytes = Math.max(node.usedBytes, position + length); |
| } |
| return length; |
| }, |
| |
| llseek(stream, offset, whence) { |
| var position = offset; |
| if (whence === {{{ cDefs.SEEK_CUR }}}) { |
| position += stream.position; |
| } else if (whence === {{{ cDefs.SEEK_END }}}) { |
| if (FS.isFile(stream.node.mode)) { |
| position += stream.node.usedBytes; |
| } |
| } |
| if (position < 0) { |
| throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); |
| } |
| return position; |
| }, |
| mmap(stream, length, position, prot, flags) { |
| if (!FS.isFile(stream.node.mode)) { |
| throw new FS.ErrnoError({{{ cDefs.ENODEV }}}); |
| } |
| var ptr; |
| var allocated; |
| var contents = stream.node.contents; |
| // Only make a new copy when MAP_PRIVATE is specified. |
| if (!(flags & {{{ cDefs.MAP_PRIVATE }}}) && contents.buffer === HEAP8.buffer) { |
| // We can't emulate MAP_SHARED when the file is not backed by the |
| // buffer we're mapping to (e.g. the HEAP buffer). |
| allocated = false; |
| ptr = contents.byteOffset; |
| } else { |
| allocated = true; |
| ptr = mmapAlloc(length); |
| if (!ptr) { |
| throw new FS.ErrnoError({{{ cDefs.ENOMEM }}}); |
| } |
| if (contents) { |
| // Try to avoid unnecessary slices. |
| if (position > 0 || position + length < contents.length) { |
| if (contents.subarray) { |
| contents = contents.subarray(position, position + length); |
| } else { |
| contents = Array.prototype.slice.call(contents, position, position + length); |
| } |
| } |
| HEAP8.set(contents, ptr); |
| } |
| } |
| return { ptr, allocated }; |
| }, |
| msync(stream, buffer, offset, length, mmapFlags) { |
| MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); |
| // should we check if bytesWritten and length are the same? |
| return 0; |
| } |
| } |
| } |
| }); |
| |