current commit
261106.8 · Expose `cDefs` as JS library constants and use them directly (#28144)
/**
 * @license
 * Copyright 2013 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

addToLibrary({
  $MEMFS: {
    ops_table: null,
    mount(mount) {
      return MEMFS.createNode(null, '/', S_IFDIR | 0o777, 0);
    },
    createNode(parent, name, mode, dev = undefined) {
      if (FS.isBlkdev(mode) || FS.isFIFO(mode)) {
        // not supported
        throw new FS.ErrnoError(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.
      const 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(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(ENOENT);
          /** @suppress {checkTypes} */
          MEMFS.doesNotExistError.stack = '<generic error, no stack>';
        }
        throw MEMFS.doesNotExistError;
#endif
      },
      mknod(parent, name, mode, dev = undefined) {
        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 && 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(ENOTEMPTY);
          }
        }
        // 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(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 | S_IFLNK, 0);
        node.link = oldpath;
        return node;
      },
      readlink(node) {
        if (!FS.isLink(node.mode)) {
          throw new FS.ErrnoError(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, 'canOwn must imply no weird position inside the file');
#endif
          node.contents = buffer.subarray(offset, offset + length);
          node.usedBytes = length;
        } else if (!node.usedBytes && !position) { // 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 === SEEK_CUR) {
          position += stream.position;
        } else if (whence === SEEK_END) {
          if (FS.isFile(stream.node.mode)) {
            position += stream.node.usedBytes;
          }
        }
        if (position < 0) {
          throw new FS.ErrnoError(EINVAL);
        }
        return position;
      },
      mmap(stream, length, position, prot, flags) {
        if (!FS.isFile(stream.node.mode)) {
          throw new FS.ErrnoError(ENODEV);
        }
        var ptr;
        var allocated;
        var contents = stream.node.contents;
        // Only make a new copy when MAP_PRIVATE is specified.
        if (!(flags & 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(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 = contents.slice(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;
      }
    }
  }
});