/** * @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 = ''; } 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; } } } });