current commit
261104.5 · lib: place `value` before `done` in iterator results
'use strict';

const {
  ArrayPrototypePush,
  Error,
  ErrorCaptureStackTrace,
  FunctionPrototypeBind,
  MathMax,
  MathMin,
  Promise,
  PromisePrototypeThen,
  PromiseReject,
  PromiseResolve,
  PromiseWithResolvers,
  SafeArrayIterator,
  SafePromisePrototypeFinally,
  SafePromiseRace,
  SafeSet,
  Symbol,
  SymbolAsyncDispose,
  SymbolAsyncIterator,
  SymbolDispose,
  SymbolIterator,
  Uint8Array,
  uncurryThis,
} = primordials;

const { fs: constants } = internalBinding('constants');
const {
  F_OK,
  O_SYMLINK,
  O_WRONLY,
  S_IFMT,
  S_IFREG,
} = constants;

const binding = internalBinding('fs');
const { Buffer } = require('buffer');
const { isBuffer: BufferIsBuffer } = Buffer;
const BufferToString = uncurryThis(Buffer.prototype.toString);

const {
  AbortError,
  aggregateTwoErrors,
  codes: {
    ERR_ACCESS_DENIED,
    ERR_FS_FILE_TOO_LARGE,
    ERR_INVALID_ARG_VALUE,
    ERR_INVALID_STATE,
    ERR_METHOD_NOT_IMPLEMENTED,
    ERR_OPERATION_FAILED,
    ERR_OUT_OF_RANGE,
  },
} = require('internal/errors');
const { isArrayBufferView } = require('internal/util/types');

const {
  constants: {
    kIoMaxLength,
    kMaxUserId,
    kReadFileBufferLength,
    kReadFileUnknownBufferLength,
    kWriteFileMaxChunkSize,
  },
  collectRecursiveReaddirResult,
  copyObject,
  getDirents,
  getRecursiveDirents,
  getOptions,
  getStatFsFromBinding,
  getStatsFromBinding,
  getValidatedPath,
  getReadFileBuffer,
  getReadFileBufferByteLengthName,
  preprocessSymlinkDestination,
  stringToFlags,
  stringToSymlinkType,
  toUnixTimestamp,
  handleErrorFromBinding: handleSyncErrorFromBinding,
  validateBufferArray,
  validateCpOptions,
  validateOffsetLengthRead,
  validateOffsetLengthWrite,
  validatePosition,
  validateReadFileBufferOptions,
  validateRmOptions,
  validateRmdirOptions,
  validateStringAfterArrayBufferView,
  vfsState,
  warnOnNonPortableTemplate,
} = require('internal/fs/utils');
const { opendir } = require('internal/fs/dir');
const {
  parseFileMode,
  validateAbortSignal,
  validateBoolean,
  validateBuffer,
  validateEncoding,
  validateInteger,
  validateObject,
  validateOneOf,
  kValidateObjectAllowNullable,
} = require('internal/validators');
const pathModule = require('path');
const {
  getLazy,
  kEmptyObject,
  lazyDOMException,
  promisify,
  isWindows,
  isMacOS,
} = require('internal/util');
const { getOptionValue } = require('internal/options');
const EventEmitter = require('events');
const { StringDecoder } = require('string_decoder');
const { kFSWatchStart, watch } = require('internal/fs/watchers');
const nonNativeWatcher = require('internal/fs/recursive_watch');
const { isIterable } = require('internal/streams/utils');
const assert = require('internal/assert');

const permission = require('internal/process/permission');

const kHandle = Symbol('kHandle');
const kFd = Symbol('kFd');
const kRefs = Symbol('kRefs');
const kClosePromise = Symbol('kClosePromise');
const kCloseReason = Symbol('kCloseReason');
const kCloseResolve = Symbol('kCloseResolve');
const kCloseReject = Symbol('kCloseReject');
const kRef = Symbol('kRef');
const kUnref = Symbol('kUnref');
const kLocked = Symbol('kLocked');
const kCloseSync = Symbol('kCloseSync');

const { kUsePromises } = binding;
const { Interface } = require('internal/readline/interface');
const {
  kDeserialize, kTransfer, kTransferList, markTransferMode,
} = require('internal/worker/js_transferable');

const getDirectoryEntriesPromise = promisify(getDirents);
const validateRmOptionsPromise = promisify(validateRmOptions);

let cpPromises;
function lazyLoadCpPromises() {
  return cpPromises ??= require('internal/fs/cp/cp').cpFn;
}

// Lazy loaded to avoid circular dependency.
let fsStreams;
function lazyFsStreams() {
  return fsStreams ??= require('internal/fs/streams');
}

const lazyRimRaf = getLazy(() => require('internal/fs/rimraf').rimrafPromises);

const lazyReadableStream = getLazy(() =>
  require('internal/webstreams/readablestream').ReadableStream,
);

// Lazy loaded to avoid circular dependency with new streams.
let newStreamsPull;
let newStreamsPullSync;
let newStreamsParsePullArgs;
let newStreamsToWriterUint8Array;
let newStreamsConvertChunks;
let newStreamsGetWriterSignal;
function lazyNewStreams() {
  if (newStreamsPull === undefined) {
    const pullModule = require('internal/streams/iter/pull');
    newStreamsPull = pullModule.pull;
    newStreamsPullSync = pullModule.pullSync;
    const utils = require('internal/streams/iter/utils');
    newStreamsParsePullArgs = utils.parsePullArgs;
    newStreamsToWriterUint8Array = utils.toWriterUint8Array;
    newStreamsConvertChunks = utils.convertChunks;
    newStreamsGetWriterSignal = utils.getWriterSignal;
  }
}

// By the time the C++ land creates an error for a promise rejection (likely from a
// libuv callback), there is already no JS frames on the stack. So we need to
// wait until V8 resumes execution back to JS land before we have enough information
// to re-capture the stack trace.
function handleErrorFromBinding(error) {
  ErrorCaptureStackTrace(error, handleErrorFromBinding);
  return PromiseReject(error);
}

class FileHandle extends EventEmitter {
  #brandCheck = undefined;

  static isFileHandle(value) {
    return (value != null && typeof value === 'object' && #brandCheck in value);
  }

  /**
   * @param {InternalFSBinding.FileHandle | undefined} filehandle
   */
  constructor(filehandle) {
    super();
    markTransferMode(this, false, true);
    this[kHandle] = filehandle;
    this[kFd] = filehandle ? filehandle.fd : -1;

    this[kRefs] = 1;
    this[kClosePromise] = null;
  }

  getAsyncId() {
    return this[kHandle].getAsyncId();
  }

  get fd() {
    return this[kFd];
  }

  appendFile(data, options) {
    return fsCall(writeFile, this, data, options);
  }

  chmod(mode) {
    return fsCall(fchmod, this, mode);
  }

  chown(uid, gid) {
    return fsCall(fchown, this, uid, gid);
  }

  datasync() {
    return fsCall(fdatasync, this);
  }

  sync() {
    return fsCall(fsync, this);
  }

  read(buffer, offset, length, position) {
    return fsCall(read, this, buffer, offset, length, position);
  }

  readv(buffers, position) {
    return fsCall(readv, this, buffers, position);
  }

  readFile(options) {
    return fsCall(readFile, this, options);
  }

  readLines(options = undefined) {
    return new Interface({
      input: this.createReadStream(options),
      crlfDelay: Infinity,
    });
  }

  stat(options) {
    return fsCall(fstat, this, options);
  }

  truncate(len = 0) {
    return fsCall(ftruncate, this, len);
  }

  utimes(atime, mtime) {
    return fsCall(futimes, this, atime, mtime);
  }

  write(buffer, offset, length, position) {
    return fsCall(write, this, buffer, offset, length, position);
  }

  writev(buffers, position) {
    return fsCall(writev, this, buffers, position);
  }

  writeFile(data, options) {
    return fsCall(writeFile, this, data, options);
  }

  close = () => {
    if (this[kFd] === -1) {
      return PromiseResolve();
    }

    if (this[kClosePromise]) {
      return this[kClosePromise];
    }

    this[kRefs]--;
    if (this[kRefs] === 0) {
      this[kFd] = -1;
      this[kClosePromise] = SafePromisePrototypeFinally(
        this[kHandle].close(),
        () => { this[kClosePromise] = undefined; },
      );
    } else {
      this[kClosePromise] = SafePromisePrototypeFinally(
        new Promise((resolve, reject) => {
          this[kCloseResolve] = resolve;
          this[kCloseReject] = reject;
        }), () => {
          this[kClosePromise] = undefined;
          this[kCloseReject] = undefined;
          this[kCloseResolve] = undefined;
        },
      );
    }

    this.emit('close');
    return this[kClosePromise];
  };

  [kCloseSync]() {
    if (this[kFd] === -1) return;
    if (this[kClosePromise]) {
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    }
    this[kFd] = -1;
    this[kHandle].closeSync();
    this.emit('close');
  }

  async [SymbolAsyncDispose]() {
    await this.close();
  }

  /**
   * @typedef {import('../webstreams/readablestream').ReadableStream
   * } ReadableStream
   * @param {{ type?: 'bytes', autoClose?: boolean }} [options]
   * @returns {ReadableStream}
   */
  readableWebStream(options = kEmptyObject) {
    if (this[kFd] === -1)
      throw new ERR_INVALID_STATE('The FileHandle is closed');
    if (this[kClosePromise])
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    if (this[kLocked])
      throw new ERR_INVALID_STATE('The FileHandle is locked');
    this[kLocked] = true;

    validateObject(options, 'options');
    const {
      type = 'bytes',
      autoClose = false,
    } = options;

    validateBoolean(autoClose, 'options.autoClose');

    if (type !== 'bytes') {
      process.emitWarning(
        'A non-"bytes" options.type has no effect. A byte-oriented steam is ' +
        'always created.',
        'ExperimentalWarning',
      );
    }

    const readFn = FunctionPrototypeBind(this.read, this);
    const ondone = async () => {
      this[kUnref]();
      if (autoClose) await this.close();
    };

    const ReadableStream = lazyReadableStream();
    const readable = new ReadableStream({
      type: 'bytes',
      autoAllocateChunkSize: 16384,

      async pull(controller) {
        const view = controller.byobRequest.view;
        const { bytesRead } = await readFn(view, 0, view.byteLength);

        if (bytesRead === 0) {
          controller.close();
          await ondone();
        }

        controller.byobRequest.respond(bytesRead);
      },

      async cancel() {
        await ondone();
      },
    });


    const {
      readableStreamCancel,
    } = require('internal/webstreams/readablestream');
    this[kRef]();
    this.once('close', () => {
      readableStreamCancel(readable);
    });

    return readable;
  }

  /**
   * @typedef {import('./streams').ReadStream
   * } ReadStream
   * @param {{
   *   encoding?: string;
   *   autoClose?: boolean;
   *   emitClose?: boolean;
   *   start: number;
   *   end?: number;
   *   highWaterMark?: number;
   *   }} [options]
   * @returns {ReadStream}
   */
  createReadStream(options = undefined) {
    const { ReadStream } = lazyFsStreams();
    return new ReadStream(undefined, { ...options, fd: this });
  }

  /**
   * @typedef {import('./streams').WriteStream
   * } WriteStream
   * @param {{
   *   encoding?: string;
   *   autoClose?: boolean;
   *   emitClose?: boolean;
   *   start: number;
   *   highWaterMark?: number;
   *   flush?: boolean;
   *   }} [options]
   * @returns {WriteStream}
   */
  createWriteStream(options = undefined) {
    const { WriteStream } = lazyFsStreams();
    return new WriteStream(undefined, { ...options, fd: this });
  }

  [kTransfer]() {
    if (this[kClosePromise] || this[kRefs] > 1) {
      throw lazyDOMException('Cannot transfer FileHandle while in use',
                             'DataCloneError');
    }

    const handle = this[kHandle];
    this[kFd] = -1;
    this[kCloseReason] = 'The FileHandle has been transferred';
    this[kHandle] = null;
    this[kRefs] = 0;

    return {
      data: { handle },
      deserializeInfo: 'internal/fs/promises:FileHandle',
    };
  }

  [kTransferList]() {
    return [ this[kHandle] ];
  }

  [kDeserialize]({ handle }) {
    this[kHandle] = handle;
    this[kFd] = handle.fd;
  }

  [kRef]() {
    this[kRefs]++;
  }

  [kUnref]() {
    this[kRefs]--;
    if (this[kRefs] === 0) {
      this[kFd] = -1;
      PromisePrototypeThen(
        this[kHandle].close(),
        this[kCloseResolve],
        this[kCloseReject],
      );
    }
  }
}

if (getOptionValue('--experimental-stream-iter')) {
  const kNullPrototo = { __proto__: null };
  const kDefaultChunkSize = 131072;
  const kNone = -1;

  // Called when iteration of a pull()/pullSync() iterable actually starts.
  function lockForIteration(handle, fd) {
    if (handle[kFd] === kNone || handle[kFd] !== fd)
      throw new ERR_INVALID_STATE('The FileHandle is closed');
    if (handle[kClosePromise])
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    if (handle[kLocked])
      throw new ERR_INVALID_STATE('The FileHandle is locked');
    handle[kLocked] = true;
  }

  /**
   * Return the file contents as an AsyncIterable<Uint8Array[]> using the
   * new streams pull model. Optional transforms and options (including
   * AbortSignal) may be provided as trailing arguments, mirroring the
   * Stream.pull() signature.
   * @param {...(Function|object)} args - Optional transforms and/or options
   * @returns {AsyncIterable<Uint8Array[]>}
   */
  FileHandle.prototype.pull = function pull(...args) {
    if (this[kFd] === kNone)
      throw new ERR_INVALID_STATE('The FileHandle is closed');
    if (this[kClosePromise])
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    if (this[kLocked])
      throw new ERR_INVALID_STATE('The FileHandle is locked');

    lazyNewStreams();
    const { transforms, options = kNullPrototo } = newStreamsParsePullArgs(args);

    const {
      autoClose = false,
      chunkSize: readSize = kDefaultChunkSize,
      signal,
    } = options;
    let {
      start: pos = kNone,
      limit: remaining = kNone,
    } = options;

    const handle = this;
    const fd = this[kFd];

    validateBoolean(autoClose, 'options.autoClose');

    if (pos !== kNone) {
      validateInteger(pos, 'options.start', 0);
    }
    if (remaining !== kNone) {
      validateInteger(remaining, 'options.limit', 1);
    }
    if (readSize !== undefined) {
      validateInteger(readSize, 'options.chunkSize', 1);
    }
    if (signal !== undefined) {
      validateAbortSignal(signal, 'options.signal');
    }

    const source = {
      __proto__: null,
      async *[SymbolAsyncIterator]() {
        // The handle is locked only while the iterable is actually being
        // consumed. Locking eagerly in pull() would leave the handle locked
        // forever if the returned iterable were never iterated.
        lockForIteration(handle, fd);
        handle[kRef]();
        try {
          if (signal) {
            // Signal-aware path
            while (remaining !== 0) {
              if (signal.aborted) {
                throw signal.reason;
              }
              const toRead = remaining > 0 ?
                MathMin(readSize, remaining) : readSize;
              const buf = Buffer.allocUnsafe(toRead);
              let bytesRead;
              try {
                bytesRead =
                  (await binding.read(fd, buf, 0,
                                      toRead, pos, kUsePromises)) || 0;
              } catch (err) {
                ErrorCaptureStackTrace(err, handleErrorFromBinding);
                throw err;
              }
              if (bytesRead === 0) break;
              if (pos >= 0) pos += bytesRead;
              if (remaining > 0) remaining -= bytesRead;
              yield [bytesRead < toRead ? buf.subarray(0, bytesRead) : buf];
            }
          } else {
            // Fast path - no signal check per iteration
            while (remaining !== 0) {
              const toRead = remaining > 0 ?
                MathMin(readSize, remaining) : readSize;
              const buf = Buffer.allocUnsafe(toRead);
              let bytesRead;
              try {
                bytesRead =
                  (await binding.read(fd, buf, 0,
                                      toRead, pos, kUsePromises)) || 0;
              } catch (err) {
                ErrorCaptureStackTrace(err, handleErrorFromBinding);
                throw err;
              }
              if (bytesRead === 0) break;
              if (pos >= 0) pos += bytesRead;
              if (remaining > 0) remaining -= bytesRead;
              yield [bytesRead < toRead ? buf.subarray(0, bytesRead) : buf];
            }
          }
        } finally {
          handle[kLocked] = false;
          handle[kUnref]();
          if (autoClose) {
            await handle.close();
          }
        }
      },
    };

    // If transforms provided, wrap with pull pipeline
    if (transforms.length > 0) {
      const pullArgs = [...transforms];
      if (options) {
        ArrayPrototypePush(pullArgs, options);
      }
      return newStreamsPull(source, ...pullArgs);
    }
    return source;
  };

  /**
   * Return the file contents as an Iterable<Uint8Array[]> using synchronous
   * reads. Optional transforms and options may be provided as trailing
   * arguments, mirroring the Stream.pullSync() signature.
   * @param {...(Function|object)} args - Optional transforms and/or options
   * @returns {Iterable<Uint8Array[]>}
   */
  FileHandle.prototype.pullSync = function pullSync(...args) {
    if (this[kFd] === kNone)
      throw new ERR_INVALID_STATE('The FileHandle is closed');
    if (this[kClosePromise])
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    if (this[kLocked])
      throw new ERR_INVALID_STATE('The FileHandle is locked');

    lazyNewStreams();
    const { transforms, options = kNullPrototo } = newStreamsParsePullArgs(args);

    const {
      autoClose = false,
      chunkSize: readSize = kDefaultChunkSize,
    } = options;
    let {
      start: pos = kNone,
      limit: remaining = kNone,
    } = options;

    const handle = this;
    const fd = this[kFd];

    validateBoolean(autoClose, 'options.autoClose');

    if (pos !== kNone) {
      validateInteger(pos, 'options.start', 0);
    }
    if (remaining !== kNone) {
      validateInteger(remaining, 'options.limit', 1);
    }
    if (readSize !== undefined) {
      validateInteger(readSize, 'options.chunkSize', 1);
    }

    function cleanup() {
      handle[kLocked] = false;
      handle[kUnref]();
      if (autoClose) {
        handle[kCloseSync]();
      }
    }

    const source = {
      __proto__: null,
      [SymbolIterator]() {
        let started = false;
        let done = false;
        return {
          __proto__: null,
          next() {
            if (done) {
              return { value: undefined, done: true };
            }
            if (!started) {
              // Lock lazily, on the first read, so that an iterable that is
              // never consumed does not leave the handle locked forever.
              lockForIteration(handle, fd);
              handle[kRef]();
              started = true;
            }
            if (remaining === 0) {
              done = true;
              cleanup();
              return { value: undefined, done: true };
            }
            const toRead = remaining > 0 ?
              MathMin(readSize, remaining) : readSize;
            const buf = Buffer.allocUnsafe(toRead);
            let bytesRead;
            try {
              bytesRead = binding.read(fd, buf, 0, toRead, pos) || 0;
            } catch (err) {
              done = true;
              cleanup();
              throw err;
            }
            if (bytesRead === 0) {
              done = true;
              cleanup();
              return { value: undefined, done: true };
            }
            if (pos >= 0) pos += bytesRead;
            if (remaining > 0) remaining -= bytesRead;
            const chunk = bytesRead < toRead ?
              buf.subarray(0, bytesRead) : buf;
            return { value: [chunk], done: false };
          },
          return() {
            if (!done) {
              done = true;
              if (started) cleanup();
            }
            return { value: undefined, done: true };
          },
        };
      },
    };

    if (transforms.length > 0) {
      return newStreamsPullSync(source, ...transforms);
    }
    return source;
  };

  /**
   * Return a new-streams Writer backed by this file handle.
   * The writer uses direct binding.writeBuffer / binding.writeBuffers
   * calls, bypassing the FileHandle.write() validation chain.
   *
   * Supports writev() for batch writes (single syscall per batch).
   * Handles EAGAIN with retry (up to 5 attempts), matching WriteStream.
   * @param {{
   *   autoClose?: boolean;
   *   start?: number;
   * }} [options]
   * @returns {{ write, writev, end, fail }}
   */
  FileHandle.prototype.writer = function writer(options = kNullPrototo) {
    if (this[kFd] === kNone)
      throw new ERR_INVALID_STATE('The FileHandle is closed');
    if (this[kClosePromise])
      throw new ERR_INVALID_STATE('The FileHandle is closing');
    if (this[kLocked])
      throw new ERR_INVALID_STATE('The FileHandle is locked');

    lazyNewStreams();

    validateObject(options, 'options');
    const {
      autoClose = false,
      chunkSize: syncWriteThreshold = kDefaultChunkSize,
    } = options;
    let {
      start: pos = kNone,
      limit: bytesRemaining = kNone,
    } = options;

    const handle = this;
    const fd = this[kFd];
    let totalBytesWritten = 0;
    let closed = false;
    let closing = false;
    let pendingEnd = null;
    let endingCleanup = false;
    let errored = false;
    let error;
    let asyncPending = 0;
    let released = false;
    const pendingWrites = new SafeSet();
    // Async writes are issued one at a time, in call order. Without this,
    // overlapping write() calls would race on the file position: when no
    // `start` is given every write targets the fd's current (shared)
    // position, and partial writes are completed in follow-up syscalls.
    let writeQueue = PromiseResolve();

    validateBoolean(autoClose, 'options.autoClose');

    if (pos !== kNone) {
      validateInteger(pos, 'options.start', 0);
    }
    if (bytesRemaining !== kNone) {
      validateInteger(bytesRemaining, 'options.limit', 1);
    }
    if (syncWriteThreshold !== undefined) {
      validateInteger(syncWriteThreshold, 'options.chunkSize', 1);
    }

    this[kLocked] = true;
    handle[kRef]();

    // Write a single buffer with EAGAIN retry (up to 5 retries).
    async function writeAll(buf, offset, length, position, signal) {
      let retries = 0;
      while (length > 0) {
        const bytesWritten = (await PromisePrototypeThen(
          binding.writeBuffer(fd, buf, offset, length, position,
                              kUsePromises),
          undefined,
          handleErrorFromBinding,
        )) || 0;

        signal?.throwIfAborted();

        if (bytesWritten === 0) {
          if (++retries > 5) {
            throw new ERR_OPERATION_FAILED('write failed after retries');
          }
        } else {
          retries = 0;
        }

        totalBytesWritten += bytesWritten;
        offset += bytesWritten;
        length -= bytesWritten;
        if (position >= 0) position += bytesWritten;
      }
    }

    // Writev with EAGAIN retry. On partial write, concatenates remaining
    // buffers and falls back to writeAll (same approach as WriteStream).
    async function writevAll(buffers, position, signal) {
      let totalSize = 0;
      for (let i = 0; i < buffers.length; i++) {
        totalSize += buffers[i].byteLength;
      }

      let retries = 0;
      while (totalSize > 0) {
        const bytesWritten = (await PromisePrototypeThen(
          binding.writeBuffers(fd, buffers, position, kUsePromises),
          undefined,
          handleErrorFromBinding,
        )) || 0;

        signal?.throwIfAborted();

        if (bytesWritten === 0) {
          if (++retries > 5) {
            throw new ERR_OPERATION_FAILED('writev failed after retries');
          }
        } else {
          retries = 0;
        }

        totalBytesWritten += bytesWritten;
        totalSize -= bytesWritten;
        if (position >= 0) position += bytesWritten;

        if (totalSize > 0) {
          // Partial write - concatenate remaining and use writeAll.
          const remaining = Buffer.concat(buffers);
          const wrote = bytesWritten;
          await writeAll(remaining, wrote, remaining.length - wrote,
                         position, signal);
          return;
        }
      }
    }

    function releaseAfterFailure() {
      if (!errored || asyncPending !== 0 || released) return;
      released = true;
      handle[kLocked] = false;
      handle[kUnref]();
      if (autoClose) {
        handle[kCloseSync]();
      }
    }

    function finishEnd() {
      if (!closing || errored || asyncPending !== 0 || endingCleanup) return;
      endingCleanup = true;
      PromisePrototypeThen(cleanup(), () => {
        endingCleanup = false;
        if (errored) return;
        closing = false;
        closed = true;
        pendingEnd.resolve(totalBytesWritten);
        pendingEnd = null;
      }, (error) => {
        endingCleanup = false;
        if (errored) return;
        closing = false;
        closed = true;
        pendingEnd.reject(error);
        pendingEnd = null;
      });
    }

    function ignoreWriteQueueResult() {}

    // Run `start` once every previously queued write has settled.
    function enqueueWrite(start, signal) {
      const operation = PromisePrototypeThen(writeQueue, () => {
        // A failed writer must not touch the file again, and a write whose
        // signal aborted while it was queued must not be started.
        if (errored) throw error;
        signal?.throwIfAborted();
        return start();
      });
      writeQueue = PromisePrototypeThen(operation, ignoreWriteQueueResult,
                                        ignoreWriteQueueResult);
      return operation;
    }

    function trackOperation(operation) {
      const { promise, resolve, reject } = PromiseWithResolvers();
      const pending = { __proto__: null, reject };
      pendingWrites.add(pending);
      asyncPending++;
      PromisePrototypeThen(operation, (value) => {
        const active = pendingWrites.delete(pending);
        asyncPending--;
        releaseAfterFailure();
        if (active) resolve(value);
        finishEnd();
      }, (error) => {
        const active = pendingWrites.delete(pending);
        asyncPending--;
        releaseAfterFailure();
        if (active) reject(error);
        finishEnd();
      });
      return promise;
    }

    // Synchronous write with EAGAIN retry. Throws on I/O error.
    // Used by writeSync for the full write, and by writevSync for
    // completing a partial writev.
    function writeSyncAll(buf, offset, length, position) {
      let retries = 0;
      while (length > 0) {
        const ctx = {};
        const bytesWritten = binding.writeBuffer(
          fd, buf, offset, length, position, undefined, ctx) || 0;
        if (ctx.errno !== undefined) {
          handleSyncErrorFromBinding(ctx);
        }
        if (bytesWritten === 0) {
          if (++retries > 5) {
            throw new ERR_OPERATION_FAILED('write failed after retries');
          }
        } else {
          retries = 0;
        }
        totalBytesWritten += bytesWritten;
        offset += bytesWritten;
        length -= bytesWritten;
        if (position >= 0) position += bytesWritten;
      }
    }

    async function cleanup() {
      if (released) return;
      released = true;
      handle[kLocked] = false;
      handle[kUnref]();
      if (autoClose) {
        await handle.close();
      }
    }

    return {
      __proto__: null,
      write(chunk, options = kNullPrototo) {
        chunk = newStreamsToWriterUint8Array(chunk);
        const signal = newStreamsGetWriterSignal(options);
        if (errored) {
          return PromiseReject(error);
        }
        if (closed) {
          return PromiseReject(
            new ERR_INVALID_STATE.TypeError('The writer is closed'));
        }
        if (closing) {
          return PromiseReject(
            new ERR_INVALID_STATE.TypeError('The writer is closing'));
        }
        if (signal?.aborted) {
          return PromiseReject(signal.reason);
        }
        if (bytesRemaining >= 0 && chunk.byteLength > bytesRemaining) {
          return PromiseReject(
            new ERR_OUT_OF_RANGE('write', `<= ${bytesRemaining} bytes`,
                                 chunk.byteLength));
        }
        if (bytesRemaining > 0) bytesRemaining -= chunk.byteLength;
        const position = pos;
        if (pos >= 0) pos += chunk.byteLength;
        return trackOperation(enqueueWrite(
          () => writeAll(chunk, 0, chunk.byteLength, position, signal),
          signal));
      },

      writev(chunks, options = kNullPrototo) {
        chunks = newStreamsConvertChunks(chunks);
        const signal = newStreamsGetWriterSignal(options);
        if (errored) {
          return PromiseReject(error);
        }
        if (closed) {
          return PromiseReject(
            new ERR_INVALID_STATE.TypeError('The writer is closed'));
        }
        if (closing) {
          return PromiseReject(
            new ERR_INVALID_STATE.TypeError('The writer is closing'));
        }
        if (signal?.aborted) {
          return PromiseReject(signal.reason);
        }
        let totalSize = 0;
        for (let i = 0; i < chunks.length; i++) {
          totalSize += chunks[i].byteLength;
        }
        if (bytesRemaining >= 0 && totalSize > bytesRemaining) {
          return PromiseReject(
            new ERR_OUT_OF_RANGE('writev', `<= ${bytesRemaining} bytes`,
                                 totalSize));
        }
        if (bytesRemaining > 0) bytesRemaining -= totalSize;
        const position = pos;
        if (pos >= 0) pos += totalSize;
        return trackOperation(enqueueWrite(
          () => writevAll(chunks, position, signal), signal));
      },

      writeSync(chunk) {
        chunk = newStreamsToWriterUint8Array(chunk);
        if (errored || closed || closing || asyncPending) return false;
        const length = chunk.byteLength;
        if (length > syncWriteThreshold) return false;
        if (length === 0) return true;
        if (bytesRemaining >= 0 && length > bytesRemaining) return false;
        const position = pos;
        // First attempt - if this fails with zero bytes written,
        // return false so pipeTo can fall back to async write().
        const ctx = {};
        const bytesWritten = binding.writeBuffer(
          fd, chunk, 0, length, position, undefined, ctx) || 0;
        if (ctx.errno !== undefined) return false;
        totalBytesWritten += bytesWritten;
        if (position >= 0) {
          pos = position + bytesWritten;
        }
        if (bytesWritten === length) {
          if (bytesRemaining > 0) bytesRemaining -= length;
          return true;
        }
        // Partial write - bytes are on disk. Must complete or throw.
        // Cannot return false here because pipeTo would re-send the
        // full chunk, causing duplicate data on disk.
        writeSyncAll(chunk, bytesWritten, length - bytesWritten,
                     position >= 0 ? position + bytesWritten : -1);
        if (bytesRemaining > 0) bytesRemaining -= length;
        return true;
      },

      writevSync(chunks) {
        chunks = newStreamsConvertChunks(chunks);
        if (errored || closed || closing || asyncPending) return false;
        let totalSize = 0;
        for (let i = 0; i < chunks.length; i++) {
          totalSize += chunks[i].byteLength;
        }
        if (totalSize > syncWriteThreshold) return false;
        if (totalSize === 0) return true;
        if (bytesRemaining >= 0 && totalSize > bytesRemaining) return false;
        const position = pos;
        // writeBuffers throws on error (zero bytes written) - safe
        // to catch and return false for async fallback.
        let bytesWritten;
        try {
          bytesWritten = binding.writeBuffers(fd, chunks, position) || 0;
        } catch {
          return false;
        }
        totalBytesWritten += bytesWritten;
        if (position >= 0) {
          pos = position + bytesWritten;
        }
        if (bytesWritten === totalSize) {
          if (bytesRemaining > 0) bytesRemaining -= totalSize;
          return true;
        }
        // Partial writev - bytes are on disk. Must complete or throw.
        const rest = Buffer.concat(chunks);
        writeSyncAll(rest, bytesWritten,
                     rest.byteLength - bytesWritten,
                     position >= 0 ? position + bytesWritten : -1);
        if (bytesRemaining > 0) bytesRemaining -= totalSize;
        return true;
      },

      end(options = kNullPrototo) {
        const signal = newStreamsGetWriterSignal(options);
        if (errored) {
          return PromiseReject(error);
        }
        if (closed) {
          return PromiseResolve(totalBytesWritten);
        }
        if (closing) {
          return pendingEnd.promise;
        }
        if (signal?.aborted) {
          return PromiseReject(signal.reason);
        }
        closing = true;
        pendingEnd = PromiseWithResolvers();
        finishEnd();
        return pendingEnd.promise;
      },

      endSync() {
        if (errored) return -1;
        if (closed) return totalBytesWritten;
        if (closing) return -1;
        if (asyncPending) return -1;
        closed = true;
        released = true;
        handle[kLocked] = false;
        handle[kUnref]();
        if (autoClose) {
          handle[kCloseSync]();
        }
        return totalBytesWritten;
      },

      fail(reason) {
        if (closed || errored) return;
        errored = true;
        error = reason;
        closing = false;
        closed = true;
        pendingEnd?.reject(reason);
        pendingEnd = null;
        for (const pending of pendingWrites) {
          pending.reject(reason);
        }
        pendingWrites.clear();
        releaseAfterFailure();
      },

      [SymbolAsyncDispose]() {
        if (closing) {
          return pendingEnd?.promise ?? PromiseResolve();
        }
        if (!closed && !errored) {
          this.fail();
        }
        return PromiseResolve();
      },

      [SymbolDispose]() {
        this.fail();
      },
    };
  };
}

async function handleFdClose(fileOpPromise, closeFunc) {
  return PromisePrototypeThen(
    fileOpPromise,
    (result) => PromisePrototypeThen(closeFunc(), () => result),
    (opError) =>
      PromisePrototypeThen(
        closeFunc(),
        () => PromiseReject(opError),
        (closeError) => PromiseReject(aggregateTwoErrors(closeError, opError)),
      ),
  );
}

async function handleFdSync(fileOpPromise, handle) {
  return PromisePrototypeThen(
    fileOpPromise,
    (result) => PromisePrototypeThen(
      handle.sync(),
      () => result,
      (syncError) => PromiseReject(syncError),
    ),
    (opError) => PromiseReject(opError),
  );
}

async function fsCall(fn, handle, ...args) {
  assert(handle[kRefs] !== undefined,
         'handle must be an instance of FileHandle');

  if (handle.fd === -1) {
    // eslint-disable-next-line no-restricted-syntax
    const err = new Error(handle[kCloseReason] ?? 'file closed');
    err.code = 'EBADF';
    err.syscall = fn.name;
    throw err;
  }

  try {
    handle[kRef]();
    return await fn(handle, ...new SafeArrayIterator(args));
  } finally {
    handle[kUnref]();
  }
}

function checkAborted(signal) {
  if (signal?.aborted)
    throw new AbortError(undefined, { cause: signal.reason });
}

function rejectWithReason(signal, reject) {
  return () => {
    reject(new AbortError(undefined, { cause: signal.reason }));
  };
}

async function raceWithSignal(opPromise, signal) {
  if (!signal) return opPromise;
  const { promise: abortPromise, reject } = PromiseWithResolvers();
  // eslint-disable-next-line no-unused-vars
  using _ = EventEmitter.addAbortListener(signal,
                                          rejectWithReason(signal, reject));
  return await SafePromiseRace([opPromise, abortPromise]);
}

async function writeFileHandle(filehandle, data, signal, encoding) {
  checkAborted(signal);
  if (isCustomIterable(data)) {
    for await (const buf of data) {
      checkAborted(signal);
      const toWrite =
        isArrayBufferView(buf) ? buf : Buffer.from(buf, encoding || 'utf8');
      let remaining = toWrite.byteLength;
      while (remaining > 0) {
        const writeSize = MathMin(kWriteFileMaxChunkSize, remaining);
        const { bytesWritten } = await write(
          filehandle, toWrite, toWrite.byteLength - remaining, writeSize);
        remaining -= bytesWritten;
        checkAborted(signal);
      }
    }
    return;
  }
  data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
  let remaining = data.byteLength;
  if (remaining === 0) return;
  do {
    checkAborted(signal);
    const { bytesWritten } =
      await write(filehandle, data, 0,
                  MathMin(kWriteFileMaxChunkSize, data.byteLength));
    remaining -= bytesWritten;
    data = new Uint8Array(
      data.buffer,
      data.byteOffset + bytesWritten,
      data.byteLength - bytesWritten,
    );
  } while (remaining > 0);
}

async function readFileHandleWithUserBuffer(filehandle, options, size) {
  const signal = options?.signal;
  const encoding = options?.encoding;
  const buffer = getReadFileBuffer(options, size);
  const byteLengthName = getReadFileBufferByteLengthName(options);
  let totalRead = 0;

  while (totalRead < buffer.byteLength) {
    checkAborted(signal);

    const length = size === 0 ?
      buffer.byteLength - totalRead :
      MathMin(size - totalRead, kReadFileBufferLength);

    const bytesRead = (await PromisePrototypeThen(
      binding.read(filehandle.fd, buffer, totalRead, length, -1, kUsePromises),
      undefined,
      handleErrorFromBinding,
    )) ?? 0;

    totalRead += bytesRead;

    if (bytesRead === 0 || totalRead === size) {
      const result = buffer.subarray(0, totalRead);
      return encoding ? result.toString(encoding) : result;
    }
  }

  if (size === 0) {
    checkAborted(signal);

    const extraBuffer = Buffer.allocUnsafeSlow(1);
    const bytesRead = (await PromisePrototypeThen(
      binding.read(filehandle.fd, extraBuffer, 0, 1, -1, kUsePromises),
      undefined,
      handleErrorFromBinding,
    )) ?? 0;

    if (bytesRead !== 0) {
      throw new ERR_INVALID_ARG_VALUE(
        byteLengthName,
        buffer.byteLength,
        'is too small to contain the entire file',
      );
    }
  }

  return encoding ? buffer.toString(encoding) : buffer.subarray(0, totalRead);
}

async function readFileHandle(filehandle, options, knownRegularFileSize) {
  const signal = options?.signal;
  const encoding = options?.encoding;
  const decoder = encoding && new StringDecoder(encoding);

  checkAborted(signal);

  let size = 0;
  let length = 0;
  if (knownRegularFileSize !== undefined) {
    // Handed over by readFile() together with an already open fd.
    size = knownRegularFileSize;
    length = encoding ? MathMin(size, kReadFileBufferLength) : size;
  } else {
    const statFields = await PromisePrototypeThen(
      binding.fstat(filehandle.fd, false, kUsePromises),
      undefined,
      handleErrorFromBinding,
    );

    checkAborted(signal);

    if ((statFields[1/* mode */] & S_IFMT) === S_IFREG) {
      size = statFields[8/* size */];
      length = encoding ? MathMin(size, kReadFileBufferLength) : size;
    }
  }
  if (length === 0) {
    length = kReadFileUnknownBufferLength;
  }

  if (size > kIoMaxLength)
    throw new ERR_FS_FILE_TOO_LARGE(size);

  if (options.buffer !== undefined) {
    return readFileHandleWithUserBuffer(filehandle, options, size);
  }

  let totalRead = 0;
  const noSize = size === 0;
  let buffer = Buffer.allocUnsafeSlow(length);
  let result = '';
  let offset = 0;
  let buffers;
  const chunkedRead = length > kReadFileBufferLength;

  while (true) {
    checkAborted(signal);

    if (chunkedRead) {
      length = MathMin(size - totalRead, kReadFileBufferLength);
    }

    const bytesRead = (await PromisePrototypeThen(
      binding.read(filehandle.fd, buffer, offset, length, -1, kUsePromises),
      undefined,
      handleErrorFromBinding,
    )) ?? 0;
    totalRead += bytesRead;

    if (bytesRead === 0 ||
        totalRead === size ||
        (bytesRead !== buffer.length && !chunkedRead && !noSize)) {
      const singleRead = bytesRead === totalRead;

      const bytesToCheck = chunkedRead ? totalRead : bytesRead;

      if (bytesToCheck !== buffer.length) {
        buffer = buffer.subarray(0, bytesToCheck);
      }

      if (!encoding) {
        if (noSize && !singleRead) {
          ArrayPrototypePush(buffers, buffer);
          return Buffer.concat(buffers, totalRead);
        }
        return buffer;
      }

      if (singleRead) {
        return buffer.toString(encoding);
      }
      result += decoder.end(buffer);
      return result;
    }
    const readBuffer = bytesRead !== buffer.length ?
      buffer.subarray(0, bytesRead) :
      buffer;
    if (encoding) {
      result += decoder.write(readBuffer);
    } else if (size !== 0) {
      offset = totalRead;
    } else {
      buffers ??= [];
      // Unknown file size requires chunks.
      ArrayPrototypePush(buffers, readBuffer);
      buffer = Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength);
    }
  }
}

// All of the functions are defined as async in order to ensure that errors
// thrown cause promise rejections rather than being thrown synchronously.
async function access(path, mode = F_OK) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.access(path, mode);
    if (promise !== undefined) { await promise; return; }
  }
  return await PromisePrototypeThen(
    binding.access(getValidatedPath(path), mode, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function cp(src, dest, options) {
  options = validateCpOptions(options);
  src = getValidatedPath(src, 'src');
  dest = getValidatedPath(dest, 'dest');
  return lazyLoadCpPromises()(src, dest, options);
}

async function copyFile(src, dest, mode) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.copyFile(src, dest, mode);
    if (promise !== undefined) { await promise; return; }
  }
  return await PromisePrototypeThen(
    binding.copyFile(
      getValidatedPath(src, 'src'),
      getValidatedPath(dest, 'dest'),
      mode,
      kUsePromises,
    ),
    undefined,
    handleErrorFromBinding,
  );
}

// Note that unlike fs.open() which uses numeric file descriptors,
// fsPromises.open() uses the fs.FileHandle class.
async function open(path, flags, mode) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.promisesOpen(path, flags, mode);
    if (result !== undefined) return result;
  }
  path = getValidatedPath(path);
  const flagsNumber = stringToFlags(flags);
  mode = parseFileMode(mode, 'mode', 0o666);
  return new FileHandle(await PromisePrototypeThen(
    binding.openFileHandle(path, flagsNumber, mode, kUsePromises),
    undefined,
    handleErrorFromBinding,
  ));
}

async function read(handle, bufferOrParams, offset, length, position) {
  let buffer = bufferOrParams;
  if (!isArrayBufferView(buffer)) {
    // This is fh.read(params)
    if (bufferOrParams !== undefined) {
      validateObject(bufferOrParams, 'options', kValidateObjectAllowNullable);
    }
    ({
      buffer = Buffer.alloc(16384),
      offset = 0,
      length = buffer.byteLength - offset,
      position = null,
    } = bufferOrParams ?? kEmptyObject);

    validateBuffer(buffer);
  }

  if (offset !== null && typeof offset === 'object') {
    // This is fh.read(buffer, options)
    ({
      offset = 0,
      length = buffer.byteLength - offset,
      position = null,
    } = offset);
  }

  if (offset == null) {
    offset = 0;
  } else {
    validateInteger(offset, 'offset', 0);
  }

  length ??= buffer.byteLength - offset;
  length |= 0;

  if (position == null) {
    position = -1;
  } else {
    validatePosition(position, 'position', length);
  }

  if (length === 0)
    return { __proto__: null, bytesRead: length, buffer };

  if (buffer.byteLength === 0) {
    throw new ERR_INVALID_ARG_VALUE('buffer', buffer,
                                    'is empty and cannot be written');
  }

  validateOffsetLengthRead(offset, length, buffer.byteLength);

  const bytesRead = (await PromisePrototypeThen(
    binding.read(handle.fd, buffer, offset, length, position, kUsePromises),
    undefined,
    handleErrorFromBinding,
  )) || 0;

  return { __proto__: null, bytesRead, buffer };
}

async function readv(handle, buffers, position) {
  validateBufferArray(buffers);

  if (typeof position !== 'number')
    position = null;

  const bytesRead = (await PromisePrototypeThen(
    binding.readBuffers(handle.fd, buffers, position, kUsePromises),
    undefined,
    handleErrorFromBinding,
  )) || 0;
  return { __proto__: null, bytesRead, buffers };
}

async function write(handle, buffer, offsetOrOptions, length, position) {
  if (buffer?.byteLength === 0)
    return { __proto__: null, bytesWritten: 0, buffer };

  let offset = offsetOrOptions;
  if (isArrayBufferView(buffer)) {
    if (typeof offset === 'object') {
      ({
        offset = 0,
        length = buffer.byteLength - offset,
        position = null,
      } = offsetOrOptions ?? kEmptyObject);
    }

    if (offset == null) {
      offset = 0;
    } else {
      validateInteger(offset, 'offset', 0);
    }
    if (typeof length !== 'number')
      length = buffer.byteLength - offset;
    if (typeof position !== 'number')
      position = null;
    validateOffsetLengthWrite(offset, length, buffer.byteLength);
    const bytesWritten =
      (await PromisePrototypeThen(
        binding.writeBuffer(handle.fd, buffer, offset,
                            length, position, kUsePromises),
        undefined,
        handleErrorFromBinding,
      )) || 0;
    return { __proto__: null, bytesWritten, buffer };
  }

  validateStringAfterArrayBufferView(buffer, 'buffer');
  validateEncoding(buffer, length);
  const bytesWritten = (await PromisePrototypeThen(
    binding.writeString(handle.fd, buffer, offset, length, kUsePromises),
    undefined,
    handleErrorFromBinding,
  )) || 0;
  return { __proto__: null, bytesWritten, buffer };
}

async function writev(handle, buffers, position) {
  validateBufferArray(buffers);

  if (typeof position !== 'number')
    position = null;

  if (buffers.length === 0) {
    return { __proto__: null, bytesWritten: 0, buffers };
  }

  const bytesWritten = (await PromisePrototypeThen(
    binding.writeBuffers(handle.fd, buffers, position, kUsePromises),
    undefined,
    handleErrorFromBinding,
  )) || 0;
  return { __proto__: null, bytesWritten, buffers };
}

async function rename(oldPath, newPath) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.rename(oldPath, newPath);
    if (promise !== undefined) { await promise; return; }
  }
  oldPath = getValidatedPath(oldPath, 'oldPath');
  newPath = getValidatedPath(newPath, 'newPath');
  return await PromisePrototypeThen(
    binding.rename(oldPath, newPath, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function truncate(path, len = 0) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.truncate(path, len);
    if (promise !== undefined) { await promise; return; }
  }
  const fd = await open(path, 'r+');
  return handleFdClose(ftruncate(fd, len), fd.close);
}

async function ftruncate(handle, len = 0) {
  validateInteger(len, 'len');
  len = MathMax(0, len);
  return await PromisePrototypeThen(
    binding.ftruncate(handle.fd, len, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function rm(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.rm(path, options);
    if (promise !== undefined) { await promise; return; }
  }
  path = getValidatedPath(path);
  options = await validateRmOptionsPromise(path, options, false);
  return lazyRimRaf()(path, options);
}

async function rmdir(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.rmdir(path);
    if (promise !== undefined) { await promise; return; }
  }
  path = getValidatedPath(path);

  if (options?.recursive !== undefined) {
    throw new ERR_INVALID_ARG_VALUE(
      'options.recursive',
      options.recursive,
      'is no longer supported',
    );
  }

  options = validateRmdirOptions(options);

  return await PromisePrototypeThen(
    binding.rmdir(path, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function fdatasync(handle) {
  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.');
  }
  return await PromisePrototypeThen(
    binding.fdatasync(handle.fd, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function fsync(handle) {
  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.');
  }
  return await PromisePrototypeThen(
    binding.fsync(handle.fd, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function mkdir(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.mkdir(path, options);
    if (promise !== undefined) return (await promise).result;
  }
  if (typeof options === 'number' || typeof options === 'string') {
    options = { mode: options };
  }
  const {
    recursive = false,
    mode = 0o777,
  } = options || kEmptyObject;
  path = getValidatedPath(path);
  validateBoolean(recursive, 'options.recursive');

  return await PromisePrototypeThen(
    binding.mkdir(
      path,
      parseFileMode(mode, 'mode', 0o777),
      recursive,
      kUsePromises,
    ),
    undefined,
    handleErrorFromBinding,
  );
}

async function readdirRecursive(originalPath, options) {
  if (permission.isEnabled()) {
    return readdirRecursiveWithPermissionModel(originalPath, options);
  }
  const withFileTypes = !!options.withFileTypes;
  const result = await PromisePrototypeThen(
    binding.readdirRecursive(
      originalPath,
      options.encoding,
      withFileTypes,
      kUsePromises,
    ),
    undefined,
    handleErrorFromBinding,
  );
  return withFileTypes ? getRecursiveDirents(originalPath, result) : result;
}

// TODO: native?
async function readdirRecursiveWithPermissionModel(basePath, options) {
  const { encoding } = options;
  const context = {
    withFileTypes: !!options.withFileTypes,
    results: [],
    dirs: [basePath],
    prefixes: [''],
  };
  for (let i = 0; i < context.dirs.length; i++) {
    const dir = context.dirs[i];
    const result = await PromisePrototypeThen(
      binding.readdir(dir, encoding, true, kUsePromises),
      undefined,
      handleErrorFromBinding,
    );
    collectRecursiveReaddirResult(dir, context.prefixes[i], result, context);
  }
  return context.results;
}

async function readdir(path, options) {
  options = getOptions(options);

  // Make shallow copy to prevent mutating options from affecting results
  options = copyObject(options);

  path = getValidatedPath(path);

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.readdir(path, options);
    if (promise !== undefined) return await promise;
  }
  if (options.recursive) {
    return readdirRecursive(path, options);
  }
  const result = await PromisePrototypeThen(
    binding.readdir(
      path,
      options.encoding,
      !!options.withFileTypes,
      kUsePromises,
    ),
    undefined,
    handleErrorFromBinding,
  );
  return options.withFileTypes ?
    getDirectoryEntriesPromise(path, result) :
    result;
}

async function readlink(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.readlink(path, options);
    if (promise !== undefined) return await promise;
  }
  options = getOptions(options);
  path = getValidatedPath(path, 'oldPath');
  return await PromisePrototypeThen(
    binding.readlink(path, options.encoding, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function symlink(target, path, type) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.symlink(target, path, type);
    if (promise !== undefined) { await promise; return; }
  }
  validateOneOf(type, 'type', ['dir', 'file', 'junction', null, undefined]);
  if (isWindows && type == null) {
    try {
      const absoluteTarget = pathModule.resolve(`${path}`, '..', `${target}`);
      type = (await stat(absoluteTarget)).isDirectory() ? 'dir' : 'file';
    } catch {
      // Default to 'file' if path is invalid or file does not exist
      type = 'file';
    }
  }

  // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass
  // the permission model security guarantees. Thus, this API is disabled unless fs.read
  // and fs.write permission has been given.
  if (permission.isEnabled() && !permission.has('fs') &&
      !permission.isAuditMode()) {
    throw new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.');
  }

  target = getValidatedPath(target, 'target');
  path = getValidatedPath(path);
  return await PromisePrototypeThen(
    binding.symlink(
      preprocessSymlinkDestination(target, type, path),
      path,
      stringToSymlinkType(type),
      kUsePromises,
    ),
    undefined,
    handleErrorFromBinding,
  );
}

async function fstat(handle, options = { __proto__: null, bigint: false }) {
  validateObject(options, 'options');
  const { signal } = options;
  if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
  checkAborted(signal);
  const result = await raceWithSignal(
    PromisePrototypeThen(
      binding.fstat(handle.fd, options.bigint, kUsePromises),
      undefined,
      handleErrorFromBinding,
    ),
    signal,
  );
  return getStatsFromBinding(result);
}

async function lstat(path, options = { __proto__: null, bigint: false }) {
  validateObject(options, 'options');
  const { signal } = options;
  if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
  checkAborted(signal);
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.lstat(path, options);
    if (promise !== undefined) return await promise;
  }
  path = getValidatedPath(path);
  if (permission.isEnabled() && !permission.has('fs.read', path) &&
      !permission.isAuditMode()) {
    const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path);
    throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource);
  }
  const result = await raceWithSignal(
    PromisePrototypeThen(
      binding.lstat(path, options.bigint, kUsePromises),
      undefined,
      handleErrorFromBinding,
    ),
    signal,
  );
  return getStatsFromBinding(result);
}

async function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
  validateObject(options, 'options');
  const { signal } = options;
  if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
  checkAborted(signal);
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.stat(path, options);
    if (promise !== undefined) return await promise;
  }
  const result = await raceWithSignal(
    PromisePrototypeThen(
      binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry),
      undefined,
      handleErrorFromBinding,
    ),
    signal,
  );

  // Binding will resolve undefined if UV_ENOENT or UV_ENOTDIR and throwIfNoEntry is false
  if (!options.throwIfNoEntry && result === undefined) return undefined;

  return getStatsFromBinding(result);
}

async function statfs(path, options = { __proto__: null, bigint: false }) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.statfs(path, options);
    if (result !== undefined) return result;
  }

  const result = await PromisePrototypeThen(
    binding.statfs(getValidatedPath(path), options.bigint, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
  return getStatFsFromBinding(result);
}

async function link(existingPath, newPath) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.link(existingPath, newPath);
    if (promise !== undefined) { await promise; return; }
  }
  existingPath = getValidatedPath(existingPath, 'existingPath');
  newPath = getValidatedPath(newPath, 'newPath');
  return await PromisePrototypeThen(
    binding.link(existingPath, newPath, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function unlink(path) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.unlink(path);
    if (promise !== undefined) { await promise; return; }
  }
  return await PromisePrototypeThen(
    binding.unlink(getValidatedPath(path), kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function fchmod(handle, mode) {
  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.');
  }
  mode = parseFileMode(mode, 'mode');
  return await PromisePrototypeThen(
    binding.fchmod(handle.fd, mode, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function chmod(path, mode) {
  path = getValidatedPath(path);
  mode = parseFileMode(mode, 'mode');

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.chmod(path, mode);
    if (promise !== undefined) { await promise; return; }
  }

  return await PromisePrototypeThen(
    binding.chmod(path, mode, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function lchmod(path, mode) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.lchmod(path, mode);
    if (promise !== undefined) { await promise; return; }
  }

  if (O_SYMLINK === undefined)
    throw new ERR_METHOD_NOT_IMPLEMENTED('lchmod()');

  const fd = await open(path, O_WRONLY | O_SYMLINK);
  return handleFdClose(fchmod(fd, mode), fd.close);
}

async function lchown(path, uid, gid) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.lchown(path, uid, gid);
    if (promise !== undefined) { await promise; return; }
  }

  path = getValidatedPath(path);
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);
  return await PromisePrototypeThen(
    binding.lchown(path, uid, gid, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function fchown(handle, uid, gid) {
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);
  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.');
  }
  return await PromisePrototypeThen(
    binding.fchown(handle.fd, uid, gid, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function chown(path, uid, gid) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.chown(path, uid, gid);
    if (promise !== undefined) { await promise; return; }
  }

  path = getValidatedPath(path);
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);
  return await PromisePrototypeThen(
    binding.chown(path, uid, gid, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function utimes(path, atime, mtime) {
  path = getValidatedPath(path);
  atime = toUnixTimestamp(atime);
  mtime = toUnixTimestamp(mtime);

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.utimes(path, atime, mtime);
    if (promise !== undefined) { await promise; return; }
  }

  return await PromisePrototypeThen(
    binding.utimes(path, atime, mtime, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function futimes(handle, atime, mtime) {
  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.');
  }
  atime = toUnixTimestamp(atime, 'atime');
  mtime = toUnixTimestamp(mtime, 'mtime');
  return await PromisePrototypeThen(
    binding.futimes(handle.fd, atime, mtime, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function lutimes(path, atime, mtime) {
  path = getValidatedPath(path);
  atime = toUnixTimestamp(atime);
  mtime = toUnixTimestamp(mtime);

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.lutimes(path, atime, mtime);
    if (promise !== undefined) { await promise; return; }
  }

  return await PromisePrototypeThen(
    binding.lutimes(path, atime, mtime, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function realpath(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.realpath(path, options);
    if (promise !== undefined) return await promise;
  }
  options = getOptions(options);
  return await PromisePrototypeThen(
    binding.realpath(getValidatedPath(path), options.encoding, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function mkdtemp(prefix, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.mkdtemp(prefix, options);
    if (promise !== undefined) return await promise;
  }
  options = getOptions(options);
  if (BufferIsBuffer(prefix)) {
    options = { ...options, encoding: 'buffer' };
  }
  prefix = getValidatedPath(prefix, 'prefix');
  warnOnNonPortableTemplate(prefix);

  return await PromisePrototypeThen(
    binding.mkdtemp(prefix, options.encoding, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
}

async function mkstemp(prefix, options) {
  options = getOptions(options);
  if (BufferIsBuffer(prefix)) {
    options = { ...options, encoding: 'buffer' };
  }
  prefix = getValidatedPath(prefix, 'prefix');
  warnOnNonPortableTemplate(prefix);

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.promisesMkstemp(prefix, options);
    if (promise !== undefined) return await promise;
  }

  const result = await PromisePrototypeThen(
    binding.mkstemp(prefix, options.encoding, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
  return { __proto__: null, path: result[0], handle: new FileHandle(result[1]) };
}

async function mkdtempDisposable(prefix, options) {
  options = getOptions(options);
  if (BufferIsBuffer(prefix)) {
    options = { ...options, encoding: 'buffer' };
  }
  prefix = getValidatedPath(prefix, 'prefix');
  warnOnNonPortableTemplate(prefix);

  const cwd = process.cwd();
  const path = await PromisePrototypeThen(
    binding.mkdtemp(prefix, options.encoding, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );
  // Stash the full path in case of process.chdir()
  const fullPath = pathModule.resolve(cwd, path);

  const remove = async () => {
    const rmrf = lazyRimRaf();
    await rmrf(fullPath, {
      maxRetries: 0,
      recursive: true,
      retryDelay: 0,
    });
  };
  return {
    __proto__: null,
    path,
    remove,
    async [SymbolAsyncDispose]() {
      await remove();
    },
  };
}

async function writeFile(path, data, options) {
  options = getOptions(options, {
    encoding: 'utf8',
    mode: 0o666,
    flag: 'w',
    flush: false,
  });
  const flush = options.flush ?? false;
  validateBoolean(flush, 'options.flush');
  parseFileMode(options.mode, 'mode', 0o666);

  const h = vfsState.handlers;
  if (h !== null) {
    checkAborted(options.signal);
    const promise = h.writeFile(path, data, options);
    if (promise !== undefined) { await promise; return; }
  }

  const flag = options.flag || 'w';

  if (!isArrayBufferView(data) && !isCustomIterable(data)) {
    validateStringAfterArrayBufferView(data, 'data');
    data = Buffer.from(data, options.encoding || 'utf8');
  }

  validateAbortSignal(options.signal);
  if (path instanceof FileHandle)
    return writeFileHandle(path, data, options.signal, options.encoding);

  checkAborted(options.signal);

  if (!flush && !isCustomIterable(data) && data.byteLength <= kWriteFileMaxChunkSize) {
    path = getValidatedPath(path);
    await writeFileInOneRoundTrip(path, stringToFlags(flag, 'options.flag'),
                                  parseFileMode(options.mode, 'mode', 0o666), data);
    checkAborted(options.signal);  // An abort during the write still wins.
    return;
  }

  const fd = await open(path, flag, options.mode);
  let writeOp = writeFileHandle(fd, data, options.signal, options.encoding);

  if (flush) {
    writeOp = handleFdSync(writeOp, fd);
  }

  return handleFdClose(writeOp, fd.close);
}

/**
 * Open + write + close as one thread pool round trip.
 * @param {string|Buffer} path Validated path
 * @param {number} flagsNumber
 * @param {number} mode
 * @param {ArrayBufferView} data
 * @returns {Promise<void>}
 */
function writeFileInOneRoundTrip(path, flagsNumber, mode, data) {
  return new Promise((resolve, reject) => {
    const job = new binding.WriteFileJob(path, flagsNumber, mode, data);
    job.ondone = (err) => {
      if (err != null) {
        ErrorCaptureStackTrace(err, writeFileInOneRoundTrip);
        reject(err);
      } else {
        resolve();
      }
    };
    const accessError = job.run(path);
    if (accessError !== undefined) {
      ErrorCaptureStackTrace(accessError, writeFileInOneRoundTrip);
      reject(accessError);
    }
  });
}

function isCustomIterable(obj) {
  return isIterable(obj) && !isArrayBufferView(obj) && typeof obj !== 'string';
}

async function appendFile(path, data, options) {
  options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
  parseFileMode(options.mode, 'mode', 0o666);

  const h = vfsState.handlers;
  if (h !== null) {
    checkAborted(options.signal);
    const promise = h.appendFile(path, data, options);
    if (promise !== undefined) { await promise; return; }
  }
  options = copyObject(options);
  options.flag ||= 'a';
  return writeFile(path, data, options);
}

async function readFile(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    checkAborted(options?.signal);
    const result = h.readFile(path, options);
    if (result !== undefined) return result;
  }
  options = getOptions(options, { flag: 'r' });
  validateReadFileBufferOptions(options);
  const flag = options.flag || 'r';

  if (path instanceof FileHandle)
    return readFileHandle(path, options);

  checkAborted(options.signal);

  if (options.buffer === undefined && vfsState.handlers === null) {
    // Open + fstat + read + close in one thread pool round trip for files of
    // up to one chunk; larger files come back as an open fd + size and are
    // read by readFileHandle() as before.
    path = getValidatedPath(path);
    const { 0: buffer, 1: fd, 2: size } = await readFileInOneRoundTrip(path, stringToFlags(flag));
    if (fd === -1) {
      checkAborted(options.signal);  // An abort during the read still wins.
      return options.encoding ? buffer.toString(options.encoding) : buffer;
    }
    const filehandle = new FileHandle(new binding.FileHandle(fd));
    return handleFdClose(readFileHandle(filehandle, options, size), filehandle.close);
  }

  const fd = await open(path, flag, 0o666);
  return handleFdClose(readFileHandle(fd, options), fd.close);
}

/**
 * @param {string|Buffer} path Validated path
 * @param {number} flagsNumber
 * @returns {Promise<[Buffer|undefined, number, number|undefined]>} [buffer, -1] or [undefined, fd, size]
 */
function readFileInOneRoundTrip(path, flagsNumber) {
  return new Promise((resolve, reject) => {
    const job = new binding.ReadFileJob(path, flagsNumber, kReadFileBufferLength);
    job.ondone = (err, buffer, fd, size, closeErr) => {
      const error = err ?? closeErr;
      if (error != null) {
        ErrorCaptureStackTrace(error, readFileInOneRoundTrip);
        reject(error);
      } else {
        resolve([buffer, fd, size]);
      }
    };
    const accessError = job.run(path);
    if (accessError !== undefined) {
      ErrorCaptureStackTrace(accessError, readFileInOneRoundTrip);
      reject(accessError);
    }
  });
}

async function* _watch(filename, options = kEmptyObject) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.promisesWatch(filename, options);
    if (result !== undefined) {
      yield* result;
      return;
    }
  }
  validateObject(options, 'options');

  if (options.recursive != null) {
    validateBoolean(options.recursive, 'options.recursive');

    // TODO(anonrig): Remove non-native watcher when/if libuv supports recursive.
    // As of November 2022, libuv does not support recursive file watch on all platforms,
    // e.g. Linux due to the limitations of inotify.
    if (options.recursive && !isMacOS && !isWindows) {
      const watcher = new nonNativeWatcher.FSWatcher(options);
      watcher[kFSWatchStart](filename);
      yield* watcher;
      return;
    }
  }

  yield* watch(filename, options);
}

const lazyGlob = getLazy(() => require('internal/fs/glob').Glob);
async function* glob(pattern, options) {
  const Glob = lazyGlob();
  yield* new Glob(pattern, options).glob();
}

module.exports = {
  exports: {
    access,
    copyFile,
    cp,
    glob,
    open,
    opendir: promisify(opendir),
    rename,
    truncate,
    rm,
    rmdir,
    mkdir,
    readdir,
    readlink,
    symlink,
    lstat,
    stat,
    statfs,
    link,
    unlink,
    chmod,
    lchmod,
    lchown,
    chown,
    utimes,
    lutimes,
    realpath,
    mkdtemp,
    mkdtempDisposable,
    mkstemp,
    writeFile,
    appendFile,
    readFile,
    watch: _watch,
    constants,
  },

  FileHandle,
  kHandle,
  kLocked,
  kRef,
  kUnref,
};