@ 261105.16
current
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

// Maintainers, keep in mind that ES1-style octal literals (`0666`) are not
// allowed in strict mode. Use ES6-style octal literals instead (`0o666`).

'use strict';

const {
  ArrayPrototypePush,
  BigIntPrototypeToString,
  FunctionPrototypeCall,
  MathMax,
  Number,
  ObjectDefineProperties,
  ObjectDefineProperty,
  Promise,
  PromisePrototypeThen,
  PromiseResolve,
  ReflectApply,
  SafeMap,
  SafeSet,
  StringPrototypeCharCodeAt,
  StringPrototypeIndexOf,
  StringPrototypeSlice,
  SymbolDispose,
  uncurryThis,
} = primordials;

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

const pathModule = require('path');
const { isArrayBufferView } = require('internal/util/types');

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,
  },
} = require('internal/errors');

const {
  FSReqCallback,
  ReadFileJob,
  WriteFileJob,
} = binding;
const { toPathIfFileURL } = require('internal/url');
const {
  customPromisifyArgs: kCustomPromisifyArgsSymbol,
  getLazy,
  kEmptyObject,
  promisify: {
    custom: kCustomPromisifiedSymbol,
  },
  SideEffectFreeRegExpPrototypeExec,
  defineLazyProperties,
  isWindows,
  isMacOS,
} = require('internal/util');
const {
  constants: {
    kIoMaxLength,
    kReadFileBufferLength,
    kMaxUserId,
  },
  collectRecursiveReaddirResult,
  copyObject,
  Dirent,
  encodeRealpathResult,
  getDirents,
  getRecursiveDirents,
  getOptions,
  getValidatedFd,
  getValidatedPath,
  handleErrorFromBinding,
  preprocessSymlinkDestination,
  Stats,
  getReadFileBuffer,
  getReadFileBufferByteLengthName,
  getStatFsFromBinding,
  getStatsFromBinding,
  realpathCacheKey,
  stringToFlags,
  stringToSymlinkType,
  toUnixTimestamp,
  validateBufferArray,
  validateCpOptions,
  validateOffsetLengthRead,
  validateOffsetLengthWrite,
  validatePath,
  validatePosition,
  validateReadFileBufferOptions,
  validateRmOptions,
  validateRmOptionsSync,
  validateRmdirOptions,
  validateStringAfterArrayBufferView,
  vfsState,
  warnOnNonPortableTemplate,
} = require('internal/fs/utils');
const {
  CHAR_FORWARD_SLASH,
  CHAR_BACKWARD_SLASH,
} = require('internal/constants');
const {
  isInt32,
  parseFileMode,
  validateAbortSignal,
  validateBoolean,
  validateBuffer,
  validateEncoding,
  validateFunction,
  validateInteger,
  validateObject,
  validateOneOf,
  validateString,
  kValidateObjectAllowNullable,
} = require('internal/validators');

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

let fs;

// Lazy loaded
let cpFn;
let cpSyncFn;
let promises = null;
let ReadStream;
let WriteStream;
let rimraf;
let kResistStopPropagation;
let ReadFileContext;

// These have to be separate because of how graceful-fs happens to do it's
// monkeypatching.
let FileReadStream;
let FileWriteStream;
let Utf8Stream;

function lazyLoadUtf8Stream() {
  Utf8Stream ??= require('internal/streams/fast-utf8-stream');
}

// Ensure that callbacks run in the global context. Only use this function
// for callbacks that are passed to the binding layer, callbacks that are
// invoked from JS already run in the proper scope.
function makeCallback(cb) {
  validateFunction(cb, 'cb');

  return (...args) => ReflectApply(cb, this, args);
}

// Special case of `makeCallback()` that is specific to async `*stat()` calls as
// an optimization, since the data passed back to the callback needs to be
// transformed anyway.
function makeStatsCallback(cb) {
  validateFunction(cb, 'cb');

  return (err, stats) => {
    if (err) return cb(err);
    if (stats === undefined && err === null) return cb(null, undefined);
    cb(err, getStatsFromBinding(stats));
  };
}

const isFd = isInt32;

/**
 * Route VFS async result (Promise) to error-first callback.
 * @param {Promise|undefined} promise The VFS handler result
 * @param {Function} callback The error-first callback
 * @returns {boolean} true if VFS handled, false otherwise
 */
function vfsResult(promise, callback) {
  if (promise === undefined) return false;
  PromisePrototypeThen(promise, (result) => callback(null, result), callback);
  return true;
}

/**
 * Route VFS async void result to error-first callback.
 * @param {Promise|undefined} promise The VFS handler result
 * @param {Function} callback The error-first callback
 * @returns {boolean} true if VFS handled, false otherwise
 */
function vfsVoid(promise, callback) {
  if (promise === undefined) return false;
  PromisePrototypeThen(promise, () => callback(null), callback);
  return true;
}

function isFileType(stats, fileType) {
  // Use stats array directly to avoid creating an fs.Stats instance just for
  // our internal use.
  let mode = stats[1];
  if (typeof mode === 'bigint')
    mode = Number(mode);
  return (mode & S_IFMT) === fileType;
}

/**
 * Tests a user's permissions for the file or directory
 * specified by `path`.
 * @param {string | Buffer | URL} path
 * @param {number} [mode]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function access(path, mode, callback) {
  if (typeof mode === 'function') {
    callback = mode;
    mode = F_OK;
  }

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.access(path, mode), callback)) return;

  path = getValidatedPath(path);
  callback = makeCallback(callback);

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.access(path, mode, req);
}

/**
 * Synchronously tests a user's permissions for the file or
 * directory specified by `path`.
 * @param {string | Buffer | URL} path
 * @param {number} [mode]
 * @returns {void}
 */
function accessSync(path, mode) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.accessSync(path, mode);
    if (result !== undefined) return;
  }
  binding.access(getValidatedPath(path), mode);
}

/**
 * Tests whether or not the given path exists.
 * @param {string | Buffer | URL} path
 * @param {(exists?: boolean) => any} callback
 * @returns {void}
 */
function exists(path, callback) {
  validateFunction(callback, 'cb');

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.existsSync(path);
    if (result !== undefined) {
      process.nextTick(callback, result);
      return;
    }
  }

  function suppressedCallback(err) {
    callback(!err);
  }

  try {
    fs.access(path, F_OK, suppressedCallback);
  } catch {
    return callback(false);
  }
}

ObjectDefineProperty(exists, kCustomPromisifiedSymbol, {
  __proto__: null,
  value: function exists(path) {
    return new Promise((resolve) => fs.exists(path, resolve));
  },
});

let showExistsDeprecation = true;
/**
 * Synchronously tests whether or not the given path exists.
 * @param {string | Buffer | URL} path
 * @returns {boolean}
 */
function existsSync(path) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.existsSync(path);
    if (result !== undefined) return result;
  }
  try {
    path = getValidatedPath(path);
  } catch (err) {
    if (showExistsDeprecation && err?.code === 'ERR_INVALID_ARG_TYPE') {
      process.emitWarning(
        'Passing invalid argument types to fs.existsSync is deprecated', 'DeprecationWarning', 'DEP0187',
      );
      showExistsDeprecation = false;
    }
    return false;
  }

  return binding.existsSync(path);
}

function readFileAfterOpen(err, fd) {
  const context = this.context;

  if (err) {
    context.callback(err);
    return;
  }

  context.fd = fd;

  const req = new FSReqCallback();
  req.oncomplete = readFileAfterStat;
  req.context = context;
  binding.fstat(fd, false, req);
}

function readFileAfterStat(err, stats) {
  const context = this.context;

  if (err)
    return context.close(err);

  // TODO(BridgeAR): Check if allocating a smaller chunk is better performance
  // wise, similar to the promise based version (less peak memory and chunked
  // stringify operations vs multiple C++/JS boundary crossings).
  const size = context.size = isFileType(stats, S_IFREG) ? stats[8] : 0;

  if (size > kIoMaxLength) {
    err = new ERR_FS_FILE_TOO_LARGE(size);
    return context.close(err);
  }

  try {
    context.prepare();
  } catch (err) {
    return context.close(err);
  }
  context.read();
}

function checkAborted(signal, callback) {
  if (signal?.aborted) {
    callback(new AbortError(undefined, { cause: signal.reason }));
    return true;
  }
  return false;
}

function bindSignalToReq(req, signal, callback) {
  if (!signal) {
    req.oncomplete = callback;
    return;
  }
  let aborted = false;
  const onAbort = () => {
    aborted = true;
    req.cancel();
    callback(new AbortError(undefined, { cause: signal.reason }));
  };
  kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
  signal.addEventListener('abort', onAbort, { __proto__: null, [kResistStopPropagation]: true });
  req.oncomplete = function(err, result) {
    signal.removeEventListener('abort', onAbort);
    if (aborted) return;
    callback(err, result);
  };
}

/**
 * Asynchronously reads the entire contents of a file.
 * @param {string | Buffer | URL | number} path
 * @param {{
 *   encoding?: string | null;
 *   flag?: string;
 *   signal?: AbortSignal;
 *   } | string} [options]
 * @param {(
 *   err?: Error,
 *   data?: string | Buffer
 *   ) => any} callback
 * @returns {void}
 */
function readFile(path, options, callback) {
  callback ||= options;
  validateFunction(callback, 'cb');

  const h = vfsState.handlers;
  if (h !== null) {
    const opts = typeof options === 'function' ? undefined : options;
    if (checkAborted(opts?.signal, callback)) return;
    if (vfsResult(h.readFile(path, opts), callback)) return;
  }

  options = getOptions(options, { flag: 'r' });
  validateReadFileBufferOptions(options);
  ReadFileContext ??= require('internal/fs/read/context');
  const context = new ReadFileContext(callback, options);
  context.isUserFd = isFd(path); // File descriptor ownership

  if (options.signal) {
    context.signal = options.signal;
  }
  if (context.isUserFd) {
    process.nextTick(function tick(context) {
      FunctionPrototypeCall(readFileAfterOpen, { context }, null, path);
    }, context);
    return;
  }

  if (checkAborted(options.signal, callback))
    return;

  const flagsNumber = stringToFlags(options.flag, 'options.flag');
  path = getValidatedPath(path);
  if (options.buffer === undefined) {
    // 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 take
    // the chunked reader below (readFileAfterOneShot). `true`: a handed-back
    // fd will be closed through fs.close(), so track it as unmanaged.
    const job = new ReadFileJob(path, flagsNumber, kReadFileBufferLength, true);
    job.context = context;
    job.ondone = readFileAfterOneShot;
    const accessError = job.run(path);
    if (accessError !== undefined) {
      // Not scheduled: report it the way the request-based open() did.
      callback(accessError);
    }
    return;
  }
  const req = new FSReqCallback();
  req.context = context;
  req.oncomplete = readFileAfterOpen;
  binding.open(path, flagsNumber, 0o666, req);
}

function readFileAfterOneShot(err, buffer, fd, size, closeErr) {
  const context = this.context;
  if (err) {
    context.callback(err);
    return;
  }
  if (fd !== -1) {
    // (context.read() below performs the abort check for this case.)
    // Larger than one chunk: continue exactly like after open + fstat.
    context.fd = fd;
    context.size = size;
    if (size > kIoMaxLength) {
      return context.close(new ERR_FS_FILE_TOO_LARGE(size));
    }
    try {
      context.prepare();
    } catch (err) {
      return context.close(err);
    }
    context.read();
    return;
  }
  if (closeErr) {
    context.callback(closeErr);
    return;
  }
  if (context.signal?.aborted) {
    // An abort that arrived while the read was in flight wins, as it did when
    // it was noticed between the open/fstat/read steps.
    context.callback(new AbortError(undefined, { cause: context.signal.reason }));
    return;
  }
  let result = buffer;
  if (context.encoding) {
    try {
      result = buffer.toString(context.encoding);
    } catch (err) {
      context.callback(err);
      return;
    }
  }
  context.callback(null, result);
}

function tryStatSync(fd, isUserFd) {
  const stats = binding.fstat(fd, false, undefined, true /* shouldNotThrow */);
  if (stats === undefined && !isUserFd) {
    fs.closeSync(fd);
  }
  return stats;
}

function tryCreateBuffer(size, fd, isUserFd) {
  let threw = true;
  let buffer;
  try {
    if (size > kIoMaxLength) {
      throw new ERR_FS_FILE_TOO_LARGE(size);
    }
    buffer = Buffer.allocUnsafe(size);
    threw = false;
  } finally {
    if (threw && !isUserFd) fs.closeSync(fd);
  }
  return buffer;
}

function tryGetReadFileBuffer(options, size, fd, isUserFd) {
  let threw = true;
  let buffer;
  try {
    buffer = getReadFileBuffer(options, size);
    threw = false;
  } finally {
    if (threw && !isUserFd) fs.closeSync(fd);
  }
  return buffer;
}

function tryReadSync(fd, isUserFd, buffer, pos, len) {
  let threw = true;
  let bytesRead;
  try {
    bytesRead = fs.readSync(fd, buffer, pos, len);
    threw = false;
  } finally {
    if (threw && !isUserFd) fs.closeSync(fd);
  }
  return bytesRead;
}

function tryReadSyncWithUserBuffer(fd, isUserFd, buffer, byteLengthName) {
  let pos = 0;
  let bytesRead = 0;

  while (pos < buffer.byteLength) {
    bytesRead = tryReadSync(fd, isUserFd, buffer, pos, buffer.byteLength - pos);
    pos += bytesRead;

    if (bytesRead === 0) {
      return pos;
    }
  }

  const extraBuffer = tryCreateBuffer(1, fd, isUserFd);
  bytesRead = tryReadSync(fd, isUserFd, extraBuffer, 0, 1);

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

  return pos;
}

/**
 * Synchronously reads the entire contents of a file.
 * @param {string | Buffer | URL | number} path
 * @param {{
 *   encoding?: string | null;
 *   flag?: string;
 *   }} [options]
 * @returns {string | Buffer}
 */
function readFileSync(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.readFileSync(path, options);
    if (result !== undefined) return result;
  }
  options = getOptions(options, { flag: 'r' });
  validateReadFileBufferOptions(options);
  const hasUserBuffer = options.buffer !== undefined;

  if ((options.encoding === 'utf8' || options.encoding === 'utf-8') &&
      !hasUserBuffer) {
    if (isInt32(path)) {
      // V8 does not report -0 as an int32, so it would reach the binding as a
      // path instead of a file descriptor.
      path |= 0;
    } else {
      path = getValidatedPath(path);
    }
    return binding.readFileUtf8(path, stringToFlags(options.flag));
  }

  const isUserFd = isFd(path); // File descriptor ownership
  const fd = isUserFd ? path : fs.openSync(path, options.flag, 0o666);

  const stats = tryStatSync(fd, isUserFd);
  const size = isFileType(stats, S_IFREG) ? stats[8] : 0;
  let pos = 0;
  let buffer; // Single buffer with file data
  let buffers; // List for when size is unknown

  if (hasUserBuffer) {
    buffer = tryGetReadFileBuffer(options, size, fd, isUserFd);
  } else if (size === 0) {
    buffers = [];
  } else {
    buffer = tryCreateBuffer(size, fd, isUserFd);
  }

  let bytesRead;

  if (hasUserBuffer) {
    if (size !== 0) {
      do {
        bytesRead = tryReadSync(fd, isUserFd, buffer, pos, size - pos);
        pos += bytesRead;
      } while (bytesRead !== 0 && pos < size);
    } else {
      pos = tryReadSyncWithUserBuffer(
        fd,
        isUserFd,
        buffer,
        getReadFileBufferByteLengthName(options),
      );
    }
  } else if (size !== 0) {
    do {
      bytesRead = tryReadSync(fd, isUserFd, buffer, pos, size - pos);
      pos += bytesRead;
    } while (bytesRead !== 0 && pos < size);
  } else {
    do {
      // The kernel lies about many files.
      // Go ahead and try to read some bytes.
      buffer = Buffer.allocUnsafe(8192);
      bytesRead = tryReadSync(fd, isUserFd, buffer, 0, 8192);
      if (bytesRead !== 0) {
        ArrayPrototypePush(buffers, buffer.slice(0, bytesRead));
      }
      pos += bytesRead;
    } while (bytesRead !== 0);
  }

  if (!isUserFd)
    fs.closeSync(fd);

  if (hasUserBuffer) {
    buffer = buffer.subarray(0, pos);
  } else if (size === 0) {
    // Data was collected into the buffers list.
    buffer = Buffer.concat(buffers, pos);
  } else if (pos < size) {
    buffer = buffer.slice(0, pos);
  }

  if (options.encoding) buffer = buffer.toString(options.encoding);
  return buffer;
}

function defaultCloseCallback(err) {
  if (err != null) throw err;
}

/**
 * Closes the file descriptor.
 * @param {number} fd
 * @param {(err?: Error) => any} [callback]
 * @returns {void}
 */
function close(fd, callback = defaultCloseCallback) {
  if (callback !== defaultCloseCallback)
    callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.close(fd), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.close(fd, req);
}

/**
 * Synchronously closes the file descriptor.
 * @param {number} fd
 * @returns {void}
 */
function closeSync(fd) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.closeSync(fd);
    if (result !== undefined) return;
  }
  binding.close(fd);
}

/**
 * Asynchronously opens a file.
 * @param {string | Buffer | URL} path
 * @param {string | number} [flags]
 * @param {string | number} [mode]
 * @param {(
 *   err?: Error,
 *   fd?: number
 *   ) => any} callback
 * @returns {void}
 */
function open(path, flags, mode, callback) {
  if (arguments.length < 3) {
    callback = flags;
    flags = 'r';
    mode = 0o666;
  } else if (typeof mode === 'function') {
    callback = mode;
    mode = 0o666;
  } else {
    mode = parseFileMode(mode, 'mode', 0o666);
  }

  const flagsNumber = stringToFlags(flags);

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.open(path, flagsNumber, mode), callback)) return;

  path = getValidatedPath(path);
  callback = makeCallback(callback);

  const req = new FSReqCallback();
  req.oncomplete = callback;

  binding.open(path, flagsNumber, mode, req);
}

/**
 * Synchronously opens a file.
 * @param {string | Buffer | URL} path
 * @param {string | number} [flags]
 * @param {string | number} [mode]
 * @returns {number}
 */
function openSync(path, flags, mode) {
  flags = stringToFlags(flags);
  mode = parseFileMode(mode, 'mode', 0o666);
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.openSync(path, flags, mode);
    if (result !== undefined) return result;
  }
  return binding.open(
    getValidatedPath(path),
    flags,
    mode,
  );
}

/**
 * @param {string | Buffer | URL } path
 * @param {{
 *   type?: string;
 *   }} [options]
 * @returns {Promise<Blob>}
 */
function openAsBlob(path, options = kEmptyObject) {
  validateObject(options, 'options');
  const { type = '' } = options;
  validateString(type, 'options.type');

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.openAsBlob(path, options);
    if (result !== undefined) return PromiseResolve(result);
  }

  // The underlying implementation here returns the Blob synchronously for now.
  // To give ourselves flexibility to maybe return the Blob asynchronously,
  // this API returns a Promise.
  path = getValidatedPath(path);
  const { createBlobFromFilePath } = require('internal/blob');
  return PromiseResolve(createBlobFromFilePath(path, { type }));
}

/**
 * @param {string | Buffer | URL} path
 * @param {{
 *   type?: string;
 *   }} [options]
 * @returns {Blob}
 */
function openAsBlobSync(path, options = kEmptyObject) {
  validateObject(options, 'options');
  const { type = '' } = options;
  validateString(type, 'options.type');
  path = getValidatedPath(path);

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.openAsBlobSync(path, options);
    if (result !== undefined) return result;
  }

  const { createBlobFromFilePath } = require('internal/blob');
  return createBlobFromFilePath(path, { type });
}

/**
 * Reads file from the specified `fd` (file descriptor).
 * @param {number} fd
 * @param {Buffer | TypedArray | DataView} buffer
 * @param {number | {
 *   offset?: number;
 *   length?: number;
 *   position?: number | bigint | null;
 *   }} [offsetOrOptions]
 * @param {number} length
 * @param {number | bigint | null} position
 * @param {(
 *   err?: Error,
 *   bytesRead?: number,
 *   buffer?: Buffer
 *   ) => any} callback
 * @returns {void}
 */
function read(fd, buffer, offsetOrOptions, length, position, callback) {
  fd = getValidatedFd(fd);
  let offset = offsetOrOptions;
  let params = null;
  if (arguments.length <= 4) {
    if (arguments.length === 4) {
      // This is fs.read(fd, buffer, options, callback)
      validateObject(offsetOrOptions, 'options', kValidateObjectAllowNullable);
      callback = length;
      params = offsetOrOptions;
    } else if (arguments.length === 3) {
      // This is fs.read(fd, bufferOrParams, callback)
      if (!isArrayBufferView(buffer)) {
        // This is fs.read(fd, params, callback)
        params = buffer;
        ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject);
      }
      callback = offsetOrOptions;
    } else {
      // This is fs.read(fd, callback)
      callback = buffer;
      buffer = Buffer.alloc(16384);
    }

    if (params !== undefined) {
      validateObject(params, 'options', kValidateObjectAllowNullable);
    }
    ({
      offset = 0,
      length = buffer?.byteLength - offset,
      position = null,
    } = params ?? kEmptyObject);
  }

  validateBuffer(buffer);
  validateFunction(callback, 'cb');

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

  length |= 0;

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

  if (length === 0) {
    return process.nextTick(function tick() {
      callback(null, 0, 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 h = vfsState.handlers;
  if (h !== null) {
    const promise = h.read(fd, buffer, offset, length, position);
    if (promise !== undefined) {
      PromisePrototypeThen(promise,
                           (bytesRead) => callback(null, bytesRead, buffer), callback);
      return;
    }
  }

  function wrapper(err, bytesRead) {
    // Retain a reference to buffer so that it can't be GC'ed too soon.
    callback(err, bytesRead || 0, buffer);
  }

  const req = new FSReqCallback();
  req.oncomplete = wrapper;

  binding.read(fd, buffer, offset, length, position, req);
}

ObjectDefineProperty(read, kCustomPromisifyArgsSymbol,
                     { __proto__: null, value: ['bytesRead', 'buffer'], enumerable: false });

/**
 * Synchronously reads the file from the
 * specified `fd` (file descriptor).
 * @param {number} fd
 * @param {Buffer | TypedArray | DataView} buffer
 * @param {number | {
 *   offset?: number;
 *   length?: number;
 *   position?: number | bigint | null;
 *   }} [offsetOrOptions]
 * @param {number} [length]
 * @param {number} [position]
 * @returns {number}
 */
function readSync(fd, buffer, offsetOrOptions, length, position) {
  validateBuffer(buffer);

  let offset = offsetOrOptions;
  if (arguments.length <= 3 || typeof offsetOrOptions === 'object') {
    if (offsetOrOptions !== undefined) {
      validateObject(offsetOrOptions, 'options', kValidateObjectAllowNullable);
    }

    ({
      offset = 0,
      length = buffer.byteLength - offset,
      position = null,
    } = offsetOrOptions ?? kEmptyObject);
  }

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

  length |= 0;

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

  if (length === 0) {
    return 0;
  }

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

  validateOffsetLengthRead(offset, length, buffer.byteLength);

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.readSync(fd, buffer, offset, length, position);
    if (result !== undefined) return result;
  }

  return binding.read(fd, buffer, offset, length, position);
}

/**
 * Reads file from the specified `fd` (file descriptor)
 * and writes to an array of `ArrayBufferView`s.
 * @param {number} fd
 * @param {ArrayBufferView[]} buffers
 * @param {number | null} [position]
 * @param {(
 *   err?: Error,
 *   bytesRead?: number,
 *   buffers?: ArrayBufferView[]
 * ) => any} callback
 * @returns {void}
 */
function readv(fd, buffers, position, callback) {
  function wrapper(err, read) {
    callback(err, read || 0, buffers);
  }

  fd = getValidatedFd(fd);
  validateBufferArray(buffers);
  callback ||= position;
  validateFunction(callback, 'cb');

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

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.readv(fd, buffers, position);
    if (promise !== undefined) {
      PromisePrototypeThen(promise,
                           (read) => callback(null, read, buffers), callback);
      return;
    }
  }

  const req = new FSReqCallback();
  req.oncomplete = wrapper;

  binding.readBuffers(fd, buffers, position, req);
}

ObjectDefineProperty(readv, kCustomPromisifyArgsSymbol,
                     { __proto__: null, value: ['bytesRead', 'buffers'], enumerable: false });

/**
 * Synchronously reads file from the
 * specified `fd` (file descriptor) and writes to an array
 * of `ArrayBufferView`s.
 * @param {number} fd
 * @param {ArrayBufferView[]} buffers
 * @param {number | null} [position]
 * @returns {number}
 */
function readvSync(fd, buffers, position) {
  validateBufferArray(buffers);

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

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.readvSync(fd, buffers, position);
    if (result !== undefined) return result;
  }

  return binding.readBuffers(fd, buffers, position);
}

/**
 * Writes `buffer` to the specified `fd` (file descriptor).
 * @param {number} fd
 * @param {Buffer | TypedArray | DataView | string} buffer
 * @param {number | object} [offsetOrOptions]
 * @param {number} [length]
 * @param {number | null} [position]
 * @param {(
 *   err?: Error,
 *   bytesWritten?: number,
 *   buffer?: Buffer | TypedArray | DataView
 * ) => any} callback
 * @returns {void}
 */
function write(fd, buffer, offsetOrOptions, length, position, callback) {
  function wrapper(err, written) {
    // Retain a reference to buffer so that it can't be GC'ed too soon.
    callback(err, written || 0, buffer);
  }

  fd = getValidatedFd(fd);
  let offset = offsetOrOptions;
  if (isArrayBufferView(buffer)) {
    callback ||= position || length || offset;
    validateFunction(callback, 'cb');

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

    if (offset == null || typeof offset === 'function') {
      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 h = vfsState.handlers;
    if (h !== null) {
      const promise = h.write(fd, buffer, offset, length, position);
      if (promise !== undefined) {
        PromisePrototypeThen(promise,
                             (bytesWritten) => callback(null, bytesWritten, buffer), callback);
        return;
      }
    }

    const req = new FSReqCallback();
    req.oncomplete = wrapper;
    binding.writeBuffer(fd, buffer, offset, length, position, req);
    return;
  }

  validateStringAfterArrayBufferView(buffer, 'buffer');

  if (typeof position !== 'function') {
    if (typeof offset === 'function') {
      position = offset;
      offset = null;
    } else {
      position = length;
    }
    length = 'utf8';
  }

  const str = buffer;
  validateEncoding(str, length);
  callback = position;
  validateFunction(callback, 'cb');

  const h = vfsState.handlers;
  if (h !== null) {
    const bufAsBuffer = Buffer.from(str, length);
    const promise = h.write(fd, bufAsBuffer, 0, bufAsBuffer.length, offset);
    if (promise !== undefined) {
      PromisePrototypeThen(promise,
                           (bytesWritten) => callback(null, bytesWritten, buffer), callback);
      return;
    }
  }

  const req = new FSReqCallback();
  req.oncomplete = wrapper;
  binding.writeString(fd, str, offset, length, req);
}

ObjectDefineProperty(write, kCustomPromisifyArgsSymbol,
                     { __proto__: null, value: ['bytesWritten', 'buffer'], enumerable: false });

/**
 * Synchronously writes `buffer` to the
 * specified `fd` (file descriptor).
 * @param {number} fd
 * @param {Buffer | TypedArray | DataView | string} buffer
 * @param {{
 *   offset?: number;
 *   length?: number;
 *   position?: number | null;
 *   }} [offsetOrOptions]
 * @param {number} [length]
 * @param {number} [position]
 * @returns {number}
 */
function writeSync(fd, buffer, offsetOrOptions, length, position) {
  const ctx = {};
  let result;

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

    const h = vfsState.handlers;
    if (h !== null) {
      const vfsResult = h.writeSync(fd, buffer, offset, length, position);
      if (vfsResult !== undefined) return vfsResult;
    }

    result = binding.writeBuffer(fd, buffer, offset, length, position,
                                 undefined, ctx);
  } else {
    validateStringAfterArrayBufferView(buffer, 'buffer');
    validateEncoding(buffer, length);

    if (offset === undefined)
      offset = null;

    const h = vfsState.handlers;
    if (h !== null) {
      const bufAsBuffer = Buffer.from(buffer, length);
      const vfsResult = h.writeSync(fd, bufAsBuffer, 0, bufAsBuffer.length, offset);
      if (vfsResult !== undefined) return vfsResult;
    }

    result = binding.writeString(fd, buffer, offset, length,
                                 undefined, ctx);
  }
  handleErrorFromBinding(ctx);
  return result;
}

/**
 * Writes an array of `ArrayBufferView`s to the
 * specified `fd` (file descriptor).
 * @param {number} fd
 * @param {ArrayBufferView[]} buffers
 * @param {number | null} [position]
 * @param {(
 *   err?: Error,
 *   bytesWritten?: number,
 *   buffers?: ArrayBufferView[]
 *   ) => any} callback
 * @returns {void}
 */
function writev(fd, buffers, position, callback) {
  function wrapper(err, written) {
    callback(err, written || 0, buffers);
  }

  fd = getValidatedFd(fd);
  validateBufferArray(buffers);
  callback ||= position;
  validateFunction(callback, 'cb');

  if (buffers.length === 0) {
    process.nextTick(callback, null, 0, buffers);
    return;
  }

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

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.writev(fd, buffers, position);
    if (promise !== undefined) {
      PromisePrototypeThen(promise,
                           (written) => callback(null, written, buffers), callback);
      return;
    }
  }

  const req = new FSReqCallback();
  req.oncomplete = wrapper;

  binding.writeBuffers(fd, buffers, position, req);
}

ObjectDefineProperty(writev, kCustomPromisifyArgsSymbol, {
  __proto__: null,
  value: ['bytesWritten', 'buffer'],
  enumerable: false,
});

/**
 * Synchronously writes an array of `ArrayBufferView`s
 * to the specified `fd` (file descriptor).
 * @param {number} fd
 * @param {ArrayBufferView[]} buffers
 * @param {number | null} [position]
 * @returns {number}
 */
function writevSync(fd, buffers, position) {
  validateBufferArray(buffers);

  if (buffers.length === 0) {
    return 0;
  }

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

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.writevSync(fd, buffers, position);
    if (result !== undefined) return result;
  }

  return binding.writeBuffers(fd, buffers, position);
}

/**
 * Asynchronously renames file at `oldPath` to
 * the pathname provided as `newPath`.
 * @param {string | Buffer | URL} oldPath
 * @param {string | Buffer | URL} newPath
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function rename(oldPath, newPath, callback) {
  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.rename(oldPath, newPath), callback)) return;

  callback = makeCallback(callback);
  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.rename(
    getValidatedPath(oldPath, 'oldPath'),
    getValidatedPath(newPath, 'newPath'),
    req,
  );
}


/**
 * Synchronously renames file at `oldPath` to
 * the pathname provided as `newPath`.
 * @param {string | Buffer | URL} oldPath
 * @param {string | Buffer | URL} newPath
 * @returns {void}
 */
function renameSync(oldPath, newPath) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.renameSync(oldPath, newPath);
    if (result !== undefined) return;
  }
  binding.rename(
    getValidatedPath(oldPath, 'oldPath'),
    getValidatedPath(newPath, 'newPath'),
  );
}

/**
 * Truncates the file.
 * @param {string | Buffer | URL} path
 * @param {number} [len]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function truncate(path, len, callback) {
  if (typeof len === 'function') {
    callback = len;
    len = 0;
  } else if (len === undefined) {
    len = 0;
  }

  validateInteger(len, 'len');
  len = MathMax(0, len);
  validateFunction(callback, 'cb');

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.truncate(path, len), callback)) return;

  fs.open(path, 'r+', (er, fd) => {
    if (er) return callback(er);
    const req = new FSReqCallback();
    req.oncomplete = function oncomplete(er) {
      fs.close(fd, (er2) => {
        callback(aggregateTwoErrors(er2, er));
      });
    };
    binding.ftruncate(fd, len, req);
  });
}

/**
 * Synchronously truncates the file.
 * @param {string | Buffer | URL} path
 * @param {number} [len]
 * @returns {void}
 */
function truncateSync(path, len) {
  if (len === undefined) {
    len = 0;
  }

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.truncateSync(path, len);
    if (result !== undefined) return;
  }

  // Allow error to be thrown, but still close fd.
  const fd = fs.openSync(path, 'r+');
  try {
    fs.ftruncateSync(fd, len);
  } finally {
    fs.closeSync(fd);
  }
}

/**
 * Truncates the file descriptor.
 * @param {number} fd
 * @param {number} [len]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function ftruncate(fd, len = 0, callback) {
  if (typeof len === 'function') {
    callback = len;
    len = 0;
  }
  validateInteger(len, 'len');
  len = MathMax(0, len);
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.ftruncate(fd, len), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.ftruncate(fd, len, req);
}

/**
 * Synchronously truncates the file descriptor.
 * @param {number} fd
 * @param {number} [len]
 * @returns {void}
 */
function ftruncateSync(fd, len = 0) {
  validateInteger(len, 'len');

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.ftruncateSync(fd, len < 0 ? 0 : len);
    if (result !== undefined) return;
  }

  binding.ftruncate(fd, len < 0 ? 0 : len);
}

function lazyLoadCp() {
  if (cpFn === undefined) {
    ({ cpFn } = require('internal/fs/cp/cp'));
    cpFn = require('util').callbackify(cpFn);
    ({ cpSyncFn } = require('internal/fs/cp/cp-sync'));
  }
}

function lazyLoadRimraf() {
  if (rimraf === undefined)
    ({ rimraf } = require('internal/fs/rimraf'));
}

/**
 * Asynchronously removes a directory.
 * @param {string | Buffer | URL} path
 * @param {object} [options]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function rmdir(path, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.rmdir(path), callback)) return;

  if (options?.recursive !== undefined) {
    // This API previously accepted a `recursive` option that was deprecated
    // and removed. However, in order to make the change more visible, we
    // opted to throw an error if recursive is specified rather than removing it
    // entirely.
    throw new ERR_INVALID_ARG_VALUE(
      'options.recursive',
      options.recursive,
      'is no longer supported',
    );
  }

  callback = makeCallback(callback);
  path = getValidatedPath(path);

  validateRmdirOptions(options);
  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.rmdir(path, req);
}

/**
 * Synchronously removes a directory.
 * @param {string | Buffer | URL} path
 * @param {object} [options]
 * @returns {void}
 */
function rmdirSync(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.rmdirSync(path);
    if (result !== undefined) return;
  }
  path = getValidatedPath(path);

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

  validateRmdirOptions(options);
  binding.rmdir(path);
}

/**
 * Asynchronously removes files and
 * directories (modeled on the standard POSIX `rm` utility).
 * @param {string | Buffer | URL} path
 * @param {{
 *   force?: boolean;
 *   maxRetries?: number;
 *   recursive?: boolean;
 *   retryDelay?: number;
 *   }} [options]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function rm(path, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.rm(path, options), callback)) return;

  path = getValidatedPath(path);

  validateRmOptions(path, options, false, (err, options) => {
    if (err) {
      return callback(err);
    }
    lazyLoadRimraf();
    return rimraf(path, options, callback);
  });
}

/**
 * Synchronously removes files and
 * directories (modeled on the standard POSIX `rm` utility).
 * @param {string | Buffer | URL} path
 * @param {{
 *   force?: boolean;
 *   maxRetries?: number;
 *   recursive?: boolean;
 *   retryDelay?: number;
 *   }} [options]
 * @returns {void}
 */
function rmSync(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.rmSync(path, options);
    if (result !== undefined) return;
  }
  const opts = validateRmOptionsSync(path, options, false);
  return binding.rmSync(getValidatedPath(path), opts.maxRetries, opts.recursive, opts.retryDelay);
}

/**
 * Forces all currently queued I/O operations associated
 * with the file to the operating system's synchronized
 * I/O completion state.
 * @param {number} fd
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function fdatasync(fd, callback) {
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.fdatasync(fd), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;

  if (permission.isEnabled()) {
    callback(new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'));
    return;
  }
  binding.fdatasync(fd, req);
}

/**
 * Synchronously forces all currently queued I/O operations
 * associated with the file to the operating
 * system's synchronized I/O completion state.
 * @param {number} fd
 * @returns {void}
 */
function fdatasyncSync(fd) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.fdatasyncSync(fd);
    if (result !== undefined) return;
  }

  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.');
  }
  binding.fdatasync(fd);
}

/**
 * Requests for all data for the open file descriptor
 * to be flushed to the storage device.
 * @param {number} fd
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function fsync(fd, callback) {
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.fsync(fd), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  if (permission.isEnabled()) {
    callback(new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'));
    return;
  }
  binding.fsync(fd, req);
}

/**
 * Synchronously requests for all data for the open
 * file descriptor to be flushed to the storage device.
 * @param {number} fd
 * @returns {void}
 */
function fsyncSync(fd) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.fsyncSync(fd);
    if (result !== undefined) return;
  }

  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.');
  }
  binding.fsync(fd);
}

/**
 * Asynchronously creates a directory.
 * @param {string | Buffer | URL} path
 * @param {{
 *   recursive?: boolean;
 *   mode?: string | number;
 *   } | number} [options]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function mkdir(path, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  const h = vfsState.handlers;
  if (h !== null) {
    const promise = h.mkdir(path, options);
    if (promise !== undefined) {
      PromisePrototypeThen(promise, (r) => callback(null, r.result), callback);
      return;
    }
  }

  let mode = 0o777;
  let recursive = false;
  if (typeof options === 'number' || typeof options === 'string') {
    mode = parseFileMode(options, 'mode');
  } else if (options) {
    if (options.recursive !== undefined) {
      recursive = options.recursive;
      validateBoolean(recursive, 'options.recursive');
    }
    if (options.mode !== undefined) {
      mode = parseFileMode(options.mode, 'options.mode');
    }
  }
  callback = makeCallback(callback);

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.mkdir(
    getValidatedPath(path),
    mode,
    recursive,
    req,
  );
}

/**
 * Synchronously creates a directory.
 * @param {string | Buffer | URL} path
 * @param {{
 *   recursive?: boolean;
 *   mode?: string | number;
 *   } | number} [options]
 * @returns {string | void}
 */
function mkdirSync(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const vfsResult = h.mkdirSync(path, options);
    if (vfsResult !== undefined) return vfsResult.result;
  }
  let mode = 0o777;
  let recursive = false;
  if (typeof options === 'number' || typeof options === 'string') {
    mode = parseFileMode(options, 'mode');
  } else if (options) {
    if (options.recursive !== undefined) {
      recursive = options.recursive;
      validateBoolean(recursive, 'options.recursive');
    }
    if (options.mode !== undefined) {
      mode = parseFileMode(options.mode, 'options.mode');
    }
  }

  const result = binding.mkdir(
    getValidatedPath(path),
    mode,
    recursive,
  );

  if (recursive) {
    return result;
  }
}

/**
 * Reads the entire contents of the `basePath` directory. This function does
 * not validate `basePath` as a directory. It is passed directly to
 * `binding.readdirRecursive`, or to `binding.readdir` for each directory when
 * the permission model is enabled, since that checks every directory on the
 * main thread where the native walk can not.
 * @param {string | Buffer} basePath
 * @param {{ encoding: string, withFileTypes: boolean }} options
 * @param {(
 *   err?: Error,
 *   files?: string[] | Buffer[] | Dirent[]
 * ) => any} callback
 * @returns {void}
 */
function readdirRecursive(basePath, options, callback) {
  if (permission.isEnabled()) {
    readdirRecursiveWithPermissionModel(basePath, options, callback);
    return;
  }
  const withFileTypes = !!options.withFileTypes;
  const req = new FSReqCallback();
  req.oncomplete = (err, result) => {
    if (err) {
      callback(err);
      return;
    }
    callback(null, withFileTypes ? getRecursiveDirents(basePath, result) : result);
  };
  binding.readdirRecursive(basePath, options.encoding, withFileTypes, req);
}

function readdirRecursiveWithPermissionModel(basePath, options, callback) {
  const { encoding } = options;
  const context = {
    withFileTypes: !!options.withFileTypes,
    results: [],
    dirs: [basePath],
    prefixes: [''],
  };

  let i = 0;

  /**
   * Reads one directory from `context.dirs` and then moves on to the next
   * one, or calls back once none are left.
   * @param {string} path
   * @param {string} prefix path of this directory relative to `basePath`
   */
  function read(path, prefix) {
    const req = new FSReqCallback();
    req.oncomplete = (err, result) => {
      if (err) {
        callback(err);
        return;
      }

      try {
        collectRecursiveReaddirResult(path, prefix, result, context);
      } catch (err) {
        callback(err);
        return;
      }

      if (i < context.dirs.length) {
        read(context.dirs[i], context.prefixes[i++]);
      } else {
        callback(null, context.results);
      }
    };

    binding.readdir(path, encoding, true, req);
  }

  read(context.dirs[i], context.prefixes[i++]);
}

/**
 * Synchronously reads the entire contents of the `basePath` directory, see
 * `readdirRecursive`.
 * @param {string | Buffer} basePath
 * @param {{ encoding: string, withFileTypes: boolean }} options
 * @returns {string[] | Buffer[] | Dirent[]}
 */
function readdirSyncRecursive(basePath, options) {
  const withFileTypes = !!options.withFileTypes;
  if (permission.isEnabled()) {
    const context = {
      withFileTypes,
      results: [],
      dirs: [basePath],
      prefixes: [''],
    };
    for (let i = 0; i < context.dirs.length; i++) {
      const dir = context.dirs[i];
      const result = binding.readdir(dir, options.encoding, true);
      collectRecursiveReaddirResult(dir, context.prefixes[i], result, context);
    }
    return context.results;
  }
  const result = binding.readdirRecursive(basePath, options.encoding, withFileTypes);
  return result !== undefined && withFileTypes ? getRecursiveDirents(basePath, result) : result;
}

/**
 * Reads the contents of a directory.
 * @param {string | Buffer | URL} path
 * @param {string | {
 *   encoding?: string;
 *   withFileTypes?: boolean;
 *   recursive?: boolean;
 *   }} [options]
 * @param {(
 *   err?: Error,
 *   files?: string[] | Buffer[] | Dirent[]
 * ) => any} callback
 * @returns {void}
 */
function readdir(path, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  callback = makeCallback(callback);
  options = getOptions(options);
  path = getValidatedPath(path);
  if (options.recursive != null) {
    validateBoolean(options.recursive, 'options.recursive');
  }

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.readdir(path, options), callback)) return;

  if (options.recursive) {
    readdirRecursive(path, options, callback);
    return;
  }

  const req = new FSReqCallback();
  if (!options.withFileTypes) {
    req.oncomplete = callback;
  } else {
    req.oncomplete = (err, result) => {
      if (err) {
        callback(err);
        return;
      }
      getDirents(path, result, callback);
    };
  }
  binding.readdir(
    path,
    options.encoding,
    !!options.withFileTypes,
    req,
  );
}

/**
 * Synchronously reads the contents of a directory.
 * @param {string | Buffer | URL} path
 * @param {string | {
 *   encoding?: string;
 *   withFileTypes?: boolean;
 *   recursive?: boolean;
 *   }} [options]
 * @returns {string | Buffer[] | Dirent[]}
 */
function readdirSync(path, options) {
  options = getOptions(options);
  path = getValidatedPath(path);
  if (options.recursive != null) {
    validateBoolean(options.recursive, 'options.recursive');
  }

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.readdirSync(path, options);
    if (result !== undefined) return result;
  }

  if (options.recursive) {
    return readdirSyncRecursive(path, options);
  }

  const result = binding.readdir(
    path,
    options.encoding,
    !!options.withFileTypes,
  );

  return result !== undefined && options.withFileTypes ? getDirents(path, result) : result;
}

/**
 * Invokes the callback with the `fs.Stats`
 * for the file descriptor.
 * @param {number} fd
 * @param {{ bigint?: boolean, signal?: AbortSignal }} [options]
 * @param {(
 *   err?: Error,
 *   stats?: Stats
 *   ) => any} [callback]
 * @returns {void}
 */
function fstat(fd, options = { __proto__: null, bigint: false }, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = kEmptyObject;
  } else if (options === null || typeof options !== 'object') {
    options = kEmptyObject;
  }

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.fstat(fd, options), callback)) return;

  callback = makeStatsCallback(callback);

  if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal');
  if (checkAborted(options.signal, callback)) return;

  const req = new FSReqCallback(options.bigint);
  bindSignalToReq(req, options.signal, callback);
  binding.fstat(fd, options.bigint, req);
}

/**
 * Retrieves the `fs.Stats` for the symbolic link
 * referred to by the `path`.
 * @param {string | Buffer | URL} path
 * @param {{ bigint?: boolean, signal?: AbortSignal }} [options]
 * @param {(
 *   err?: Error,
 *   stats?: Stats
 *   ) => any} callback
 * @returns {void}
 */
function lstat(path, options = { __proto__: null, bigint: false }, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = kEmptyObject;
  } else if (options === null || typeof options !== 'object') {
    options = kEmptyObject;
  } else {
    options = getOptions(options, { bigint: false });
  }

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.lstat(path, options), callback)) return;

  callback = makeStatsCallback(callback);
  path = getValidatedPath(path);
  if (permission.isEnabled() && !permission.has('fs.read', path) &&
      !permission.isAuditMode()) {
    const resource = BufferIsBuffer(path) ? BufferToString(path) : path;
    callback(new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource));
    return;
  }

  if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal');
  if (checkAborted(options.signal, callback)) return;

  const req = new FSReqCallback(options.bigint);
  bindSignalToReq(req, options.signal, callback);
  binding.lstat(path, options.bigint, req);
}

/**
 * Asynchronously gets the stats of a file.
 * @param {string | Buffer | URL} path
 * @param {{ bigint?: boolean, signal?: AbortSignal }} [options]
 * @param {(
 *   err?: Error,
 *   stats?: Stats
 *   ) => any} callback
 * @returns {void}
 */
function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = kEmptyObject;
  } else if (options === null || typeof options !== 'object') {
    options = kEmptyObject;
  } else {
    options = getOptions(options, { bigint: false });
  }

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.stat(path, options), callback)) return;

  callback = makeStatsCallback(callback);
  path = getValidatedPath(path);

  if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal');
  if (checkAborted(options.signal, callback)) return;

  const req = new FSReqCallback(options.bigint);
  bindSignalToReq(req, options.signal, callback);
  binding.stat(path, options.bigint, req, options.throwIfNoEntry);
}

function statfs(path, options = { __proto__: null, bigint: false }, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = kEmptyObject;
  }
  validateFunction(callback, 'cb');

  const h = vfsState.handlers;
  if (h !== null) {
    let result;
    try {
      result = h.statfsSync(path, options);
    } catch (err) {
      process.nextTick(callback, err);
      return;
    }
    if (result !== undefined) {
      process.nextTick(callback, null, result);
      return;
    }
  }

  path = getValidatedPath(path);
  const req = new FSReqCallback(options.bigint);
  req.oncomplete = (err, stats) => {
    if (err) {
      return callback(err);
    }

    callback(err, getStatFsFromBinding(stats));
  };
  binding.statfs(path, options.bigint, req);
}

/**
 * Synchronously retrieves the `fs.Stats` for
 * the file descriptor.
 * @param {number} fd
 * @param {{ bigint?: boolean; }} [options]
 * @returns {Stats | undefined}
 */
function fstatSync(fd, options = { __proto__: null, bigint: false }) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.fstatSync(fd);
    if (result !== undefined) return result;
  }
  const stats = binding.fstat(fd, options.bigint, undefined, false);
  if (stats === undefined) {
    return;
  }
  return getStatsFromBinding(stats);
}

/**
 * Synchronously retrieves the `fs.Stats` for
 * the symbolic link referred to by the `path`.
 * @param {string | Buffer | URL} path
 * @param {{
 *   bigint?: boolean;
 *   throwIfNoEntry?: boolean;
 *   }} [options]
 * @returns {Stats | undefined}
 */
function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
  const h = vfsState.handlers;
  if (h !== null) {
    try {
      const result = h.lstatSync(path, options);
      if (result !== undefined) return result;
    } catch (err) {
      if (err?.code === 'ENOENT' && options?.throwIfNoEntry === false) return;
      throw err;
    }
  }
  path = getValidatedPath(path);
  if (permission.isEnabled() && !permission.has('fs.read', path) &&
      !permission.isAuditMode()) {
    const resource = BufferIsBuffer(path) ? BufferToString(path) : path;
    throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource);
  }
  const stats = binding.lstat(
    path,
    options.bigint,
    undefined,
    options.throwIfNoEntry,
  );

  if (stats === undefined) {
    return;
  }
  return getStatsFromBinding(stats);
}

/**
 * Synchronously retrieves the `fs.Stats`
 * for the `path`.
 * @param {string | Buffer | URL} path
 * @param {{
 *   bigint?: boolean;
 *   throwIfNoEntry?: boolean;
 *   }} [options]
 * @returns {Stats}
 */
function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
  const h = vfsState.handlers;
  if (h !== null) {
    try {
      const result = h.statSync(path, options);
      if (result !== undefined) return result;
    } catch (err) {
      if (err?.code === 'ENOENT' && options?.throwIfNoEntry === false) return undefined;
      throw err;
    }
  }
  const stats = binding.stat(
    getValidatedPath(path),
    options.bigint,
    undefined,
    options.throwIfNoEntry,
  );
  if (stats === undefined) {
    return undefined;
  }
  return getStatsFromBinding(stats);
}

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

  const stats = binding.statfs(getValidatedPath(path), options.bigint);
  return getStatFsFromBinding(stats);
}

/**
 * Reads the contents of a symbolic link
 * referred to by `path`.
 * @param {string | Buffer | URL} path
 * @param {{ encoding?: string; } | string} [options]
 * @param {(
 *   err?: Error,
 *   linkString?: string | Buffer
 *   ) => any} callback
 * @returns {void}
 */
function readlink(path, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.readlink(path, options), callback)) return;

  callback = makeCallback(callback);
  options = getOptions(options);
  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.readlink(getValidatedPath(path), options.encoding, req);
}

/**
 * Synchronously reads the contents of a symbolic link
 * referred to by `path`.
 * @param {string | Buffer | URL} path
 * @param {{ encoding?: string; } | string} [options]
 * @returns {string | Buffer}
 */
function readlinkSync(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.readlinkSync(path, options);
    if (result !== undefined) return result;
  }
  options = getOptions(options);
  return binding.readlink(getValidatedPath(path), options.encoding);
}

/**
 * Creates the link called `path` pointing to `target`.
 * @param {string | Buffer | URL} target
 * @param {string | Buffer | URL} path
 * @param {string | null} [type]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function symlink(target, path, type, callback) {
  if (callback === undefined) {
    callback = makeCallback(type);
    type = undefined;
  } else {
    validateOneOf(type, 'type', ['dir', 'file', 'junction', null, undefined]);
  }

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.symlink(target, path, type), callback)) return;

  // 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()) {
    callback(new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.'));
    return;
  }

  target = getValidatedPath(target, 'target');
  path = getValidatedPath(path);

  if (isWindows && type == null) {
    let absoluteTarget;
    try {
      // Symlinks targets can be relative to the newly created path.
      // Calculate absolute file name of the symlink target, and check
      // if it is a directory. Ignore resolve error to keep symlink
      // errors consistent between platforms if invalid path is
      // provided.
      absoluteTarget = pathModule.resolve(path, '..', target);
    } catch {
      // Continue regardless of error.
    }
    if (absoluteTarget !== undefined) {
      stat(absoluteTarget, (err, stat) => {
        const resolvedType = !err && stat.isDirectory() ? 'dir' : 'file';
        const resolvedFlags = stringToSymlinkType(resolvedType);
        const destination = preprocessSymlinkDestination(target,
                                                         resolvedType,
                                                         path);

        const req = new FSReqCallback();
        req.oncomplete = callback;
        binding.symlink(
          destination,
          path,
          resolvedFlags,
          req,
        );
      });
      return;
    }
  }

  const destination = preprocessSymlinkDestination(target, type, path);

  const flags = stringToSymlinkType(type);
  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.symlink(destination, path, flags, req);
}

/**
 * Synchronously creates the link called `path`
 * pointing to `target`.
 * @param {string | Buffer | URL} target
 * @param {string | Buffer | URL} path
 * @param {string | null} [type]
 * @returns {void}
 */
function symlinkSync(target, path, type) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.symlinkSync(target, path, type);
    if (result !== undefined) return;
  }
  validateOneOf(type, 'type', ['dir', 'file', 'junction', null, undefined]);
  if (isWindows && type == null) {
    const absoluteTarget = pathModule.resolve(`${path}`, '..', `${target}`);
    if (statSync(absoluteTarget, { throwIfNoEntry: false })?.isDirectory()) {
      type = 'dir';
    }
  }

  // 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);

  binding.symlink(
    preprocessSymlinkDestination(target, type, path),
    path,
    stringToSymlinkType(type),
  );
}

/**
 * Creates a new link from the `existingPath`
 * to the `newPath`.
 * @param {string | Buffer | URL} existingPath
 * @param {string | Buffer | URL} newPath
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function link(existingPath, newPath, callback) {
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.link(existingPath, newPath), callback)) return;

  existingPath = getValidatedPath(existingPath, 'existingPath');
  newPath = getValidatedPath(newPath, 'newPath');

  const req = new FSReqCallback();
  req.oncomplete = callback;

  binding.link(existingPath, newPath, req);
}

/**
 * Synchronously creates a new link from the `existingPath`
 * to the `newPath`.
 * @param {string | Buffer | URL} existingPath
 * @param {string | Buffer | URL} newPath
 * @returns {void}
 */
function linkSync(existingPath, newPath) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.linkSync(existingPath, newPath);
    if (result !== undefined) return;
  }

  existingPath = getValidatedPath(existingPath, 'existingPath');
  newPath = getValidatedPath(newPath, 'newPath');

  binding.link(
    existingPath,
    newPath,
  );
}

/**
 * Asynchronously removes a file or symbolic link.
 * @param {string | Buffer | URL} path
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function unlink(path, callback) {
  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.unlink(path), callback)) return;

  callback = makeCallback(callback);
  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.unlink(getValidatedPath(path), req);
}

/**
 * Synchronously removes a file or symbolic link.
 * @param {string | Buffer | URL} path
 * @returns {void}
 */
function unlinkSync(path) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.unlinkSync(path);
    if (result !== undefined) return;
  }
  binding.unlink(getValidatedPath(path));
}

/**
 * Sets the permissions on the file.
 * @param {number} fd
 * @param {string | number} mode
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function fchmod(fd, mode, callback) {
  mode = parseFileMode(mode, 'mode');
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.fchmod(fd, mode), callback)) return;

  if (permission.isEnabled()) {
    callback(new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.'));
    return;
  }

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.fchmod(fd, mode, req);
}

/**
 * Synchronously sets the permissions on the file.
 * @param {number} fd
 * @param {string | number} mode
 * @returns {void}
 */
function fchmodSync(fd, mode) {
  mode = parseFileMode(mode, 'mode');

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.fchmodSync(fd, mode);
    if (result !== undefined) return;
  }

  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.');
  }
  binding.fchmod(fd, mode);
}

/**
 * Changes the permissions on a symbolic link.
 * @param {string | Buffer | URL} path
 * @param {number} mode
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function lchmod(path, mode, callback) {
  validateFunction(callback, 'cb');
  mode = parseFileMode(mode, 'mode');

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.lchmod(path, mode), callback)) return;

  fs.open(path, O_WRONLY | O_SYMLINK, (err, fd) => {
    if (err) {
      callback(err);
      return;
    }
    // Prefer to return the chmod error, if one occurs,
    // but still try to close, and report closing errors if they occur.
    fs.fchmod(fd, mode, (err) => {
      fs.close(fd, (err2) => {
        callback(aggregateTwoErrors(err2, err));
      });
    });
  });
}

/**
 * Synchronously changes the permissions on a symbolic link.
 * @param {string | Buffer | URL} path
 * @param {number} mode
 * @returns {void}
 */
function lchmodSync(path, mode) {
  const fd = fs.openSync(path, O_WRONLY | O_SYMLINK);

  // Prefer to return the chmod error, if one occurs,
  // but still try to close, and report closing errors if they occur.
  try {
    fs.fchmodSync(fd, mode);
  } finally {
    fs.closeSync(fd);
  }
}

/**
 * Asynchronously changes the permissions of a file.
 * @param {string | Buffer | URL} path
 * @param {string | number} mode
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function chmod(path, mode, callback) {
  path = getValidatedPath(path);
  mode = parseFileMode(mode, 'mode');
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.chmod(path, mode), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.chmod(path, mode, req);
}

/**
 * Synchronously changes the permissions of a file.
 * @param {string | Buffer | URL} path
 * @param {string | number} mode
 * @returns {void}
 */
function chmodSync(path, mode) {
  path = getValidatedPath(path);
  mode = parseFileMode(mode, 'mode');

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.chmodSync(path, mode);
    if (result !== undefined) return;
  }

  binding.chmod(path, mode);
}

/**
 * Sets the owner of the symbolic link.
 * @param {string | Buffer | URL} path
 * @param {number} uid
 * @param {number} gid
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function lchown(path, uid, gid, callback) {
  callback = makeCallback(callback);
  path = getValidatedPath(path);
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.lchown(path, uid, gid), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.lchown(path, uid, gid, req);
}

/**
 * Synchronously sets the owner of the symbolic link.
 * @param {string | Buffer | URL} path
 * @param {number} uid
 * @param {number} gid
 * @returns {void}
 */
function lchownSync(path, uid, gid) {
  path = getValidatedPath(path);
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.lchownSync(path, uid, gid);
    if (result !== undefined) return;
  }

  binding.lchown(path, uid, gid);
}

/**
 * Sets the owner of the file.
 * @param {number} fd
 * @param {number} uid
 * @param {number} gid
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function fchown(fd, uid, gid, callback) {
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.fchown(fd), callback)) return;

  if (permission.isEnabled()) {
    callback(new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.'));
    return;
  }

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.fchown(fd, uid, gid, req);
}

/**
 * Synchronously sets the owner of the file.
 * @param {number} fd
 * @param {number} uid
 * @param {number} gid
 * @returns {void}
 */
function fchownSync(fd, uid, gid) {
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.fchownSync(fd);
    if (result !== undefined) return;
  }

  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.');
  }

  binding.fchown(fd, uid, gid);
}

/**
 * Asynchronously changes the owner and group
 * of a file.
 * @param {string | Buffer | URL} path
 * @param {number} uid
 * @param {number} gid
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function chown(path, uid, gid, callback) {
  callback = makeCallback(callback);
  path = getValidatedPath(path);
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.chown(path, uid, gid), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.chown(path, uid, gid, req);
}

/**
 * Synchronously changes the owner and group
 * of a file.
 * @param {string | Buffer | URL} path
 * @param {number} uid
 * @param {number} gid
 * @returns {void}
 */
function chownSync(path, uid, gid) {
  path = getValidatedPath(path);
  validateInteger(uid, 'uid', -1, kMaxUserId);
  validateInteger(gid, 'gid', -1, kMaxUserId);

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.chownSync(path, uid, gid);
    if (result !== undefined) return;
  }

  binding.chown(path, uid, gid);
}

/**
 * Changes the file system timestamps of the object
 * referenced by `path`.
 * @param {string | Buffer | URL} path
 * @param {number | string | Date} atime
 * @param {number | string | Date} mtime
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function utimes(path, atime, mtime, callback) {
  callback = makeCallback(callback);
  path = getValidatedPath(path);
  atime = toUnixTimestamp(atime);
  mtime = toUnixTimestamp(mtime);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.utimes(path, atime, mtime), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.utimes(path, atime, mtime, req);
}

/**
 * Synchronously changes the file system timestamps
 * of the object referenced by `path`.
 * @param {string | Buffer | URL} path
 * @param {number | string | Date} atime
 * @param {number | string | Date} mtime
 * @returns {void}
 */
function utimesSync(path, atime, mtime) {
  path = getValidatedPath(path);
  atime = toUnixTimestamp(atime);
  mtime = toUnixTimestamp(mtime);

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

  binding.utimes(path, atime, mtime);
}

/**
 * Changes the file system timestamps of the object
 * referenced by the supplied `fd` (file descriptor).
 * @param {number} fd
 * @param {number | string | Date} atime
 * @param {number | string | Date} mtime
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function futimes(fd, atime, mtime, callback) {
  atime = toUnixTimestamp(atime, 'atime');
  mtime = toUnixTimestamp(mtime, 'mtime');
  callback = makeCallback(callback);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.futimes(fd, atime, mtime), callback)) return;

  if (permission.isEnabled()) {
    callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'));
    return;
  }

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.futimes(fd, atime, mtime, req);
}

/**
 * Synchronously changes the file system timestamps
 * of the object referenced by the
 * supplied `fd` (file descriptor).
 * @param {number} fd
 * @param {number | string | Date} atime
 * @param {number | string | Date} mtime
 * @returns {void}
 */
function futimesSync(fd, atime, mtime) {
  atime = toUnixTimestamp(atime, 'atime');
  mtime = toUnixTimestamp(mtime, 'mtime');

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.futimesSync(fd, atime, mtime);
    if (result !== undefined) return;
  }

  if (permission.isEnabled()) {
    throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.');
  }

  binding.futimes(fd, atime, mtime);
}

/**
 * Changes the access and modification times of
 * a file in the same way as `fs.utimes()`.
 * @param {string | Buffer | URL} path
 * @param {number | string | Date} atime
 * @param {number | string | Date} mtime
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function lutimes(path, atime, mtime, callback) {
  callback = makeCallback(callback);
  path = getValidatedPath(path);
  atime = toUnixTimestamp(atime);
  mtime = toUnixTimestamp(mtime);

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.lutimes(path, atime, mtime), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.lutimes(path, atime, mtime, req);
}

/**
 * Synchronously changes the access and modification
 * times of a file in the same way as `fs.utimesSync()`.
 * @param {string | Buffer | URL} path
 * @param {number | string | Date} atime
 * @param {number | string | Date} mtime
 * @returns {void}
 */
function lutimesSync(path, atime, mtime) {
  path = getValidatedPath(path);
  atime = toUnixTimestamp(atime);
  mtime = toUnixTimestamp(mtime);

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

  binding.lutimes(path, atime, mtime);
}

function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) {
  if (signal?.aborted) {
    const abortError = new AbortError(undefined, { cause: signal.reason });
    if (isUserFd) {
      callback(abortError);
    } else {
      fs.close(fd, (err) => {
        callback(aggregateTwoErrors(err, abortError));
      });
    }
    return;
  }
  // write(fd, buffer, offset, length, position, callback)
  fs.write(fd, buffer, offset, length, null, (writeErr, written) => {
    if (writeErr) {
      if (isUserFd) {
        callback(writeErr);
      } else {
        fs.close(fd, (err) => {
          callback(aggregateTwoErrors(err, writeErr));
        });
      }
    } else if (written === length) {
      if (!flush) {
        if (isUserFd) {
          callback(null);
        } else {
          fs.close(fd, callback);
        }
      } else {
        fs.fsync(fd, (syncErr) => {
          if (syncErr) {
            if (isUserFd) {
              callback(syncErr);
            } else {
              fs.close(fd, (err) => {
                callback(aggregateTwoErrors(err, syncErr));
              });
            }
          } else if (isUserFd) {
            callback(null);
          } else {
            fs.close(fd, callback);
          }
        });
      }
    } else {
      offset += written;
      length -= written;
      writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback);
    }
  });
}

/**
 * Asynchronously writes data to the file.
 * @param {string | Buffer | URL | number} path
 * @param {string | Buffer | TypedArray | DataView} data
 * @param {{
 *   encoding?: string | null;
 *   mode?: number;
 *   flag?: string;
 *   signal?: AbortSignal;
 *   flush?: boolean;
 *   } | string} [options]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function writeFile(path, data, options, callback) {
  callback ||= options;
  validateFunction(callback, 'cb');

  options = getOptions(typeof options === 'function' ? null : 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) {
    if (checkAborted(options.signal, callback)) return;
    if (vfsVoid(h.writeFile(path, data, options), callback)) return;
  }

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

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

  if (isFd(path)) {
    const isUserFd = true;
    const signal = options.signal;
    writeAll(path, isUserFd, data, 0, data.byteLength, signal, flush, callback);
    return;
  }

  if (checkAborted(options.signal, callback))
    return;

  if (!flush) {
    // Open + write + close in one thread pool round trip.
    const signal = options.signal;
    path = getValidatedPath(path);
    const job = new WriteFileJob(path, stringToFlags(flag, 'options.flag'),
                                 parseFileMode(options.mode, 'mode', 0o666), data);
    job.ondone = signal == null ? callback : (err) => {
      // An abort that arrived while the write was in flight still wins.
      callback(signal.aborted && !err ? new AbortError(undefined, { cause: signal.reason }) : err);
    };
    const accessError = job.run(path);
    if (accessError !== undefined) {
      callback(accessError);
    }
    return;
  }

  fs.open(path, flag, options.mode, (openErr, fd) => {
    if (openErr) {
      callback(openErr);
    } else {
      const isUserFd = false;
      const signal = options.signal;
      writeAll(fd, isUserFd, data, 0, data.byteLength, signal, flush, callback);
    }
  });
}

/**
 * Synchronously writes data to the file.
 * @param {string | Buffer | URL | number} path
 * @param {string | Buffer | TypedArray | DataView} data
 * @param {{
 *   encoding?: string | null;
 *   mode?: number;
 *   flag?: string;
 *   flush?: boolean;
 *   } | string} [options]
 * @returns {void}
 */
function writeFileSync(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) {
    const result = h.writeFileSync(path, data, options);
    if (result !== undefined) return;
  }

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

  // C++ fast path for string data and UTF8 encoding
  if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8')) {
    if (isInt32(path)) {
      // V8 does not report -0 as an int32, so it would reach the binding as a
      // path instead of a file descriptor.
      path |= 0;
    } else {
      path = getValidatedPath(path);
    }

    return binding.writeFileUtf8(
      path,
      data,
      stringToFlags(flag),
      parseFileMode(options.mode, 'mode', 0o666),
    );
  }

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

  const isUserFd = isFd(path); // File descriptor ownership
  const fd = isUserFd ? path : fs.openSync(path, flag, options.mode);

  let offset = 0;
  let length = data.byteLength;
  try {
    while (length > 0) {
      const written = fs.writeSync(fd, data, offset, length);
      offset += written;
      length -= written;
    }

    if (flush) {
      fs.fsyncSync(fd);
    }
  } finally {
    if (!isUserFd) fs.closeSync(fd);
  }
}

/**
 * Asynchronously appends data to a file.
 * @param {string | Buffer | URL | number} path
 * @param {string | Buffer} data
 * @param {{
 *   encoding?: string | null;
 *   mode?: number;
 *   flag?: string;
 *   flush?: boolean;
 *   } | string} [options]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function appendFile(path, data, options, callback) {
  callback ||= options;
  validateFunction(callback, 'cb');

  options = getOptions(typeof options === 'function' ? null : options, {
    encoding: 'utf8', mode: 0o666, flag: 'a',
  });
  parseFileMode(options.mode, 'mode', 0o666);

  const h = vfsState.handlers;
  if (h !== null) {
    if (checkAborted(options.signal, callback)) return;
    if (vfsVoid(h.appendFile(path, data, options), callback)) return;
  }

  // Don't make changes directly on options object
  options = copyObject(options);

  // Force append behavior when using a supplied file descriptor
  if (!options.flag || isFd(path))
    options.flag = 'a';

  fs.writeFile(path, data, options, callback);
}

/**
 * Synchronously appends data to a file.
 * @param {string | Buffer | URL | number} path
 * @param {string | Buffer} data
 * @param {{
 *   encoding?: string | null;
 *   mode?: number;
 *   flag?: string;
 *   } | string} [options]
 * @returns {void}
 */
function appendFileSync(path, data, options) {
  options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
  parseFileMode(options.mode, 'mode', 0o666);

  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.appendFileSync(path, data, options);
    if (result !== undefined) return;
  }

  // Don't make changes directly on options object
  options = copyObject(options);

  // Force append behavior when using a supplied file descriptor
  if (!options.flag || isFd(path))
    options.flag = 'a';

  fs.writeFileSync(path, data, options);
}

/**
 * Watches for the changes on `filename`.
 * @param {string | Buffer | URL} filename
 * @param {string | {
 *   persistent?: boolean;
 *   recursive?: boolean;
 *   encoding?: string;
 *   signal?: AbortSignal;
 *   throwIfNoEntry?: boolean;
 *   }} [options]
 * @param {(
 *   eventType?: string,
 *   filename?: string | Buffer
 *   ) => any} [listener]
 * @returns {watchers.FSWatcher}
 */
function watch(filename, options, listener) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.watch(filename, options, listener);
    if (result !== undefined) return result;
  }
  if (typeof options === 'function') {
    listener = options;
  }
  options = getOptions(options);

  // Don't make changes directly on options object
  options = copyObject(options);

  if (options.persistent === undefined) options.persistent = true;
  if (options.recursive === undefined) options.recursive = false;
  if (options.throwIfNoEntry === undefined) options.throwIfNoEntry = true;

  let watcher;
  const watchers = require('internal/fs/watchers');
  const path = getValidatedPath(filename);
  // 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 nonNativeWatcher = require('internal/fs/recursive_watch');
    watcher = new nonNativeWatcher.FSWatcher(options);
    watcher[watchers.kFSWatchStart](path);
  } else {
    watcher = new watchers.FSWatcher();
    watcher[watchers.kFSWatchStart](path,
                                    options.persistent,
                                    options.recursive,
                                    options.encoding,
                                    options.ignore,
                                    options.throwIfNoEntry);
  }

  if (listener) {
    watcher.addListener('change', listener);
  }
  if (options.signal) {
    if (options.signal.aborted) {
      process.nextTick(() => watcher.close());
    } else {
      const listener = () => watcher.close();
      kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
      options.signal.addEventListener('abort', listener, { __proto__: null, [kResistStopPropagation]: true });
      watcher.once('close', () => {
        options.signal.removeEventListener('abort', listener);
      });
    }
  }

  return watcher;
}


const statWatchers = new SafeMap();

/**
 * Watches for changes on `filename`.
 * @param {string | Buffer | URL} filename
 * @param {{
 *   bigint?: boolean;
 *   persistent?: boolean;
 *   interval?: number;
 *   }} [options]
 * @param {(
 *   current?: Stats,
 *   previous?: Stats
 *   ) => any} listener
 * @returns {watchers.StatWatcher}
 */
function watchFile(filename, options, listener) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.watchFile(filename, options, listener);
    if (result !== undefined) return result;
  }
  filename = getValidatedPath(filename);
  filename = pathModule.resolve(filename);
  let stat;

  if (options === null || typeof options !== 'object') {
    listener = options;
    options = null;
  }

  options = {
    // Poll interval in milliseconds. 5007 is what libev used to use. It's
    // a little on the slow side but let's stick with it for now to keep
    // behavioral changes to a minimum.
    interval: 5007,
    persistent: true,
    ...options,
  };

  validateFunction(listener, 'listener');

  stat = statWatchers.get(filename);
  const watchers = require('internal/fs/watchers');
  if (stat === undefined) {
    stat = new watchers.StatWatcher(options.bigint);
    stat[watchers.kFSStatWatcherStart](filename,
                                       options.persistent, options.interval);
    statWatchers.set(filename, stat);
  } else {
    stat[watchers.kFSStatWatcherAddOrCleanRef]('add');
  }

  stat.addListener('change', listener);
  return stat;
}

/**
 * Stops watching for changes on `filename`.
 * @param {string | Buffer | URL} filename
 * @param {() => any} [listener]
 * @returns {void}
 */
function unwatchFile(filename, listener) {
  const h = vfsState.handlers;
  if (h !== null) {
    if (h.unwatchFile(filename, listener)) return;
  }
  filename = getValidatedPath(filename);
  filename = pathModule.resolve(filename);
  const stat = statWatchers.get(filename);

  if (stat === undefined) return;
  const watchers = require('internal/fs/watchers');
  if (typeof listener === 'function') {
    const beforeListenerCount = stat.listenerCount('change');
    stat.removeListener('change', listener);
    if (stat.listenerCount('change') < beforeListenerCount)
      stat[watchers.kFSStatWatcherAddOrCleanRef]('clean');
  } else {
    stat.removeAllListeners('change');
    stat[watchers.kFSStatWatcherAddOrCleanRef]('cleanAll');
  }

  if (stat.listenerCount('change') === 0) {
    stat.stop();
    statWatchers.delete(filename);
  }
}


let splitRoot;
let getRealpathRootLstatPath;
if (isWindows) {
  // Regex to find the device root on Windows (e.g. 'c:\\'), including trailing
  // slash.
  const splitRootRe = /^(?:[a-zA-Z]:|[\\/]{2}[^\\/]+[\\/][^\\/]+)?[\\/]*/;
  const namespacedDriveRootRe = /^\\\\\?\\([a-zA-Z]:\\)$/;
  splitRoot = function splitRoot(str) {
    return SideEffectFreeRegExpPrototypeExec(splitRootRe, str)[0];
  };

  // The root probe is the only use of this path. Passing a namespaced drive
  // root to the binding would lose its trailing separator during resolution.
  getRealpathRootLstatPath = function getRealpathRootLstatPath(path) {
    const match = SideEffectFreeRegExpPrototypeExec(namespacedDriveRootRe, path);
    return match === null ? path : match[1];
  };
} else {
  splitRoot = function splitRoot(str) {
    for (let i = 0; i < str.length; ++i) {
      if (StringPrototypeCharCodeAt(str, i) !== CHAR_FORWARD_SLASH)
        return StringPrototypeSlice(str, 0, i);
    }
    return str;
  };

}

// Finds the next portion of a (partial) path, up to the next path delimiter
let nextPart;
if (isWindows) {
  nextPart = function nextPart(p, i) {
    for (; i < p.length; ++i) {
      const ch = StringPrototypeCharCodeAt(p, i);

      // Check for a separator character
      if (ch === CHAR_BACKWARD_SLASH || ch === CHAR_FORWARD_SLASH)
        return i;
    }
    return -1;
  };
} else {
  nextPart = function nextPart(p, i) {
    return StringPrototypeIndexOf(p, '/', i);
  };
}

/**
 * Returns the resolved pathname.
 * @param {string | Buffer | URL} p
 * @param {string | { encoding?: string | null; }} [options]
 * @returns {string | Buffer}
 */
function realpathSync(p, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.realpathSync(p, options);
    if (result !== undefined) return result;
  }
  options = getOptions(options);
  p = toPathIfFileURL(p);
  if (typeof p !== 'string') {
    p += '';
  }
  validatePath(p);
  p = pathModule.resolve(p);

  // Check the resolved input path up front so that the lstat walk below cannot
  // be used to probe the existence of paths outside the granted set.
  if (permission.isEnabled() && !permission.has('fs.read', p) &&
      !permission.isAuditMode()) {
    throw new ERR_ACCESS_DENIED(
      'Access to this API has been restricted. Use --allow-fs-read to manage permissions.',
      'FileSystemRead',
      p,
    );
  }

  const cache = options[realpathCacheKey];
  const maybeCachedResult = cache?.get(p);
  if (maybeCachedResult) {
    return maybeCachedResult;
  }

  const seenLinks = new SafeMap();
  const knownHard = new SafeSet();
  const original = p;
  // Whether the symlink this walk resolved last pointed at a pipe or a
  // socket, which is where the walk stops. It cannot be read back from the
  // shared stat buffer, which holds the last stat made anywhere in the
  // process rather than the last one made here.
  let reachedPipeOrSocket = false;

  // Current character position in p
  let pos;
  // The partial path so far, including a trailing slash if any
  let current;
  // The partial path without a trailing slash (except when pointing at a root)
  let base;
  // The partial path scanned in the previous round, with slash
  let previous;

  // Skip over roots
  current = base = splitRoot(p);
  pos = current.length;

  // On windows, check that the root exists. On unix there is no need.
  if (isWindows) {
    const out = binding.lstat(
      getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);
    if (out === undefined) {
      return;
    }
    knownHard.add(base);
  }

  // Walk down the path, swapping out linked path parts for their real
  // values
  // NB: p.length changes.
  while (pos < p.length) {
    // find the next part
    const result = nextPart(p, pos);
    previous = current;
    if (result === -1) {
      const last = StringPrototypeSlice(p, pos);
      current += last;
      base = previous + last;
      pos = p.length;
    } else {
      current += StringPrototypeSlice(p, pos, result + 1);
      base = previous + StringPrototypeSlice(p, pos, result);
      pos = result + 1;
    }

    // Continue if not a symlink, break if a pipe/socket
    if (knownHard.has(base) || cache?.get(base) === base) {
      if (reachedPipeOrSocket) {
        break;
      }
      continue;
    }

    let resolvedLink;
    const maybeCachedResolved = cache?.get(base);
    if (maybeCachedResolved) {
      resolvedLink = maybeCachedResolved;
    } else {
      // Use stats array directly to avoid creating an fs.Stats instance just
      // for our internal use.

      const stats = binding.lstat(base, true, undefined, true /* throwIfNoEntry */);
      if (stats === undefined) {
        return;
      }

      if (!isFileType(stats, S_IFLNK)) {
        knownHard.add(base);
        cache?.set(base, base);
        continue;
      }

      // Read the link if it wasn't read before
      // dev/ino always return 0 on windows, so skip the check.
      let linkTarget = null;
      let id;
      if (!isWindows) {
        const dev = BigIntPrototypeToString(stats[0], 32);
        const ino = BigIntPrototypeToString(stats[7], 32);
        id = `${dev}:${ino}`;
        if (seenLinks.has(id)) {
          linkTarget = seenLinks.get(id);
        }
      }
      if (linkTarget === null) {
        const targetStats = binding.stat(base, false, undefined, true);
        reachedPipeOrSocket = isFileType(targetStats, S_IFIFO) ||
          isFileType(targetStats, S_IFSOCK);
        linkTarget = binding.readlink(base, undefined);
      }
      resolvedLink = pathModule.resolve(previous, linkTarget);

      cache?.set(base, resolvedLink);
      if (!isWindows) seenLinks.set(id, linkTarget);
    }

    // Resolve the link, then start over
    p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));

    // Skip over roots
    current = base = splitRoot(p);
    pos = current.length;

    // On windows, check that the root exists. On unix there is no need.
    if (isWindows && !knownHard.has(base)) {
      const out = binding.lstat(
        getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);
      if (out === undefined) {
        return;
      }
      knownHard.add(base);
    }
  }

  cache?.set(original, p);
  return encodeRealpathResult(p, options);
}

/**
 * Returns the resolved pathname.
 * @param {string | Buffer | URL} path
 * @param {string | { encoding?: string; }} [options]
 * @returns {string | Buffer}
 */
realpathSync.native = (path, options) => {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.realpathSync(path, options);
    if (result !== undefined) return result;
  }
  options = getOptions(options);
  return binding.realpath(
    getValidatedPath(path),
    options.encoding,
  );
};

/**
 * Asynchronously computes the canonical pathname by
 * resolving `.`, `..` and symbolic links.
 * @param {string | Buffer | URL} p
 * @param {string | { encoding?: string; }} [options]
 * @param {(
 *   err?: Error,
 *   resolvedPath?: string | Buffer
 *   ) => any} callback
 * @returns {void}
 */
function realpath(p, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  } else {
    validateFunction(callback, 'cb');
  }

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.realpath(p, options), callback)) return;

  options = getOptions(options);
  p = toPathIfFileURL(p);

  if (typeof p !== 'string') {
    p += '';
  }
  validatePath(p);
  p = pathModule.resolve(p);

  const seenLinks = new SafeMap();
  const knownHard = new SafeSet();
  // Whether the symlink this walk resolved last pointed at a pipe or a
  // socket, which is where the walk stops. It cannot be read back from the
  // shared stat buffer, which holds the last stat made anywhere in the
  // process rather than the last one made here.
  let reachedPipeOrSocket = false;

  // Current character position in p
  let pos;
  // The partial path so far, including a trailing slash if any
  let current;
  // The partial path without a trailing slash (except when pointing at a root)
  let base;
  // The partial path scanned in the previous round, with slash
  let previous;

  current = base = splitRoot(p);
  pos = current.length;

  // On windows, check that the root exists. On unix there is no need.
  if (isWindows && !knownHard.has(base)) {
    fs.lstat(getRealpathRootLstatPath(base), (err) => {
      if (err) return callback(err);
      knownHard.add(base);
      LOOP();
    });
  } else {
    process.nextTick(LOOP);
  }

  // Walk down the path, swapping out linked path parts for their real
  // values
  function LOOP() {
    // Stop if scanned past end of path
    if (pos >= p.length) {
      return callback(null, encodeRealpathResult(p, options));
    }

    // find the next part
    const result = nextPart(p, pos);
    previous = current;
    if (result === -1) {
      const last = StringPrototypeSlice(p, pos);
      current += last;
      base = previous + last;
      pos = p.length;
    } else {
      current += StringPrototypeSlice(p, pos, result + 1);
      base = previous + StringPrototypeSlice(p, pos, result);
      pos = result + 1;
    }

    // Continue if not a symlink, break if a pipe/socket
    if (knownHard.has(base)) {
      if (reachedPipeOrSocket) {
        return callback(null, encodeRealpathResult(p, options));
      }
      return process.nextTick(LOOP);
    }

    return fs.lstat(base, { bigint: true }, gotStat);
  }

  function gotStat(err, stats) {
    if (err) return callback(err);

    // If not a symlink, skip to the next path part
    if (!stats.isSymbolicLink()) {
      knownHard.add(base);
      return process.nextTick(LOOP);
    }

    // Stat & read the link if not read before.
    // Call `gotTarget()` as soon as the link target is known.
    // `dev`/`ino` always return 0 on windows, so skip the check.
    let id;
    if (!isWindows) {
      const dev = BigIntPrototypeToString(stats.dev, 32);
      const ino = BigIntPrototypeToString(stats.ino, 32);
      id = `${dev}:${ino}`;
      if (seenLinks.has(id)) {
        return gotTarget(null, seenLinks.get(id));
      }
    }
    fs.stat(base, (err, targetStats) => {
      if (err) return callback(err);

      reachedPipeOrSocket = targetStats.isFIFO() || targetStats.isSocket();

      fs.readlink(base, (err, target) => {
        if (!isWindows) seenLinks.set(id, target);
        gotTarget(err, target);
      });
    });
  }

  function gotTarget(err, target) {
    if (err) return callback(err);

    gotResolvedLink(pathModule.resolve(previous, target));
  }

  function gotResolvedLink(resolvedLink) {
    // Resolve the link, then start over
    p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));
    current = base = splitRoot(p);
    pos = current.length;

    // On windows, check that the root exists. On unix there is no need.
    if (isWindows && !knownHard.has(base)) {
      fs.lstat(getRealpathRootLstatPath(base), (err) => {
        if (err) return callback(err);
        knownHard.add(base);
        LOOP();
      });
    } else {
      process.nextTick(LOOP);
    }
  }
}

/**
 * Asynchronously computes the canonical pathname by
 * resolving `.`, `..` and symbolic links.
 * @param {string | Buffer | URL} path
 * @param {string | { encoding?: string; }} [options]
 * @param {(
 *   err?: Error,
 *   resolvedPath?: string | Buffer
 *   ) => any} callback
 * @returns {void}
 */
realpath.native = (path, options, callback) => {
  callback = makeCallback(callback || options);
  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.realpath(path, options), callback)) return;
  options = getOptions(options);
  path = getValidatedPath(path);
  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.realpath(path, options.encoding, req);
};

/**
 * Creates a unique temporary directory.
 * @param {string | Buffer | URL} prefix
 * @param {string | { encoding?: string; }} [options]
 * @param {(
 *   err?: Error,
 *   directory?: string
 *   ) => any} callback
 * @returns {void}
 */
function mkdtemp(prefix, options, callback) {
  callback = makeCallback(typeof options === 'function' ? options : callback);

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.mkdtemp(prefix, typeof options === 'function' ? undefined : options), callback)) return;

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

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.mkdtemp(prefix, options.encoding, req);
}

/**
 * Synchronously creates a unique temporary directory.
 * @param {string | Buffer | URL} prefix
 * @param {string | { encoding?: string; }} [options]
 * @returns {string}
 */
function mkdtempSync(prefix, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.mkdtempSync(prefix, options);
    if (result !== undefined) return result;
  }

  options = getOptions(options);
  if (BufferIsBuffer(prefix)) {
    options = { ...options, encoding: 'buffer' };
  }
  prefix = getValidatedPath(prefix, 'prefix');
  warnOnNonPortableTemplate(prefix);
  return binding.mkdtemp(prefix, options.encoding);
}

/**
 * Synchronously creates a unique temporary directory.
 * The returned value is a disposable object which removes the
 * directory and its contents when disposed.
 * @param {string | Buffer | URL} prefix
 * @param {string | { encoding?: string; }} [options]
 * @returns {object} A disposable object with a "path" property.
 */
function mkdtempDisposableSync(prefix, options) {
  options = getOptions(options);
  if (BufferIsBuffer(prefix)) {
    options = { ...options, encoding: 'buffer' };
  }
  prefix = getValidatedPath(prefix, 'prefix');
  warnOnNonPortableTemplate(prefix);

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

  const remove = () => {
    binding.rmSync(fullPath, 0 /* maxRetries */, true /* recursive */, 100 /* retryDelay */);
  };
  return {
    path,
    remove,
    [SymbolDispose]() {
      remove();
    },
  };
}

/**
 * Creates and opens a unique temporary file.
 * @param {string | Buffer | URL} prefix
 * @param {string | { encoding?: string; }} [options]
 * @param {(err?: Error, file?: { path: string | Buffer, fd: number }) => any} callback
 * @returns {void}
 */
function mkstemp(prefix, options, callback) {
  callback = makeCallback(typeof options === 'function' ? options : callback);

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

  const h = vfsState.handlers;
  if (h !== null && vfsResult(h.mkstemp(prefix, options), callback)) return;

  const req = new FSReqCallback();
  req.oncomplete = (err, result) => {
    if (err) return callback(err);
    callback(null, { path: result[0], fd: result[1] });
  };
  binding.mkstemp(prefix, options.encoding, req);
}

/**
 * Synchronously creates and opens a unique temporary file.
 * @param {string | Buffer | URL} prefix
 * @param {string | { encoding?: string; }} [options]
 * @returns {{ path: string | Buffer, fd: number }}
 */
function mkstempSync(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 result = h.mkstempSync(prefix, options);
    if (result !== undefined) return result;
  }

  const result = binding.mkstemp(prefix, options.encoding);
  return { path: result[0], fd: result[1] };
}

/**
 * Asynchronously copies `src` to `dest`. By
 * default, `dest` is overwritten if it already exists.
 * @param {string | Buffer | URL} src
 * @param {string | Buffer | URL} dest
 * @param {number} [mode]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function copyFile(src, dest, mode, callback) {
  if (typeof mode === 'function') {
    callback = mode;
    mode = 0;
  }

  const h = vfsState.handlers;
  if (h !== null && vfsVoid(h.copyFile(src, dest, mode), callback)) return;

  src = getValidatedPath(src, 'src');
  dest = getValidatedPath(dest, 'dest');
  callback = makeCallback(callback);

  const req = new FSReqCallback();
  req.oncomplete = callback;
  binding.copyFile(src, dest, mode, req);
}

/**
 * Synchronously copies `src` to `dest`. By
 * default, `dest` is overwritten if it already exists.
 * @param {string | Buffer | URL} src
 * @param {string | Buffer | URL} dest
 * @param {number} [mode]
 * @returns {void}
 */
function copyFileSync(src, dest, mode) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.copyFileSync(src, dest, mode);
    if (result !== undefined) return;
  }
  binding.copyFile(
    getValidatedPath(src, 'src'),
    getValidatedPath(dest, 'dest'),
    mode,
  );
}

/**
 * Asynchronously copies `src` to `dest`. `src` can be a file, directory, or
 * symlink. The contents of directories will be copied recursively.
 * @param {string | URL} src
 * @param {string | URL} dest
 * @param {object} [options]
 * @param {(err?: Error) => any} callback
 * @returns {void}
 */
function cp(src, dest, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }
  callback = makeCallback(callback);
  options = validateCpOptions(options);
  src = getValidatedPath(src, 'src');
  dest = getValidatedPath(dest, 'dest');
  lazyLoadCp();
  cpFn(src, dest, options, callback);
}

/**
 * Synchronously copies `src` to `dest`. `src` can be a file, directory, or
 * symlink. The contents of directories will be copied recursively.
 * @param {string | URL} src
 * @param {string | URL} dest
 * @param {object} [options]
 * @returns {void}
 */
function cpSync(src, dest, options) {
  options = validateCpOptions(options);
  src = getValidatedPath(src, 'src');
  dest = getValidatedPath(dest, 'dest');
  lazyLoadCp();
  cpSyncFn(src, dest, options);
}

function lazyLoadStreams() {
  if (!ReadStream) {
    ({ ReadStream, WriteStream } = require('internal/fs/streams'));
    FileReadStream = ReadStream;
    FileWriteStream = WriteStream;
  }
}

/**
 * Creates a readable stream with a default `highWaterMark`
 * of 64 KiB.
 * @param {string | Buffer | URL} path
 * @param {string | {
 *   flags?: string;
 *   encoding?: string;
 *   fd?: number | FileHandle;
 *   mode?: number;
 *   autoClose?: boolean;
 *   emitClose?: boolean;
 *   start: number;
 *   end?: number;
 *   highWaterMark?: number;
 *   fs?: object | null;
 *   signal?: AbortSignal | null;
 *   }} [options]
 * @returns {ReadStream}
 */
function createReadStream(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.createReadStream(path, options);
    if (result !== undefined) return result;
  }
  lazyLoadStreams();
  return new ReadStream(path, options);
}

/**
 * Creates a write stream.
 * @param {string | Buffer | URL} path
 * @param {string | {
 *   flags?: string;
 *   encoding?: string;
 *   fd?: number | FileHandle;
 *   mode?: number;
 *   autoClose?: boolean;
 *   emitClose?: boolean;
 *   start: number;
 *   fs?: object | null;
 *   signal?: AbortSignal | null;
 *   highWaterMark?: number;
 *   flush?: boolean;
 *   }} [options]
 * @returns {WriteStream}
 */
function createWriteStream(path, options) {
  const h = vfsState.handlers;
  if (h !== null) {
    const result = h.createWriteStream(path, options);
    if (result !== undefined) return result;
  }
  lazyLoadStreams();
  return new WriteStream(path, options);
}

const lazyGlob = getLazy(() => require('internal/fs/glob').Glob);

function glob(pattern, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }
  callback = makeCallback(callback);

  const Glob = lazyGlob();
  PromisePrototypeThen(
    new Glob(pattern, options).globAll(),
    (res) => callback(null, res),
    callback,
  );
}

function globSync(pattern, options) {
  const Glob = lazyGlob();
  return new Glob(pattern, options).globSync();
}


module.exports = fs = {
  appendFile,
  appendFileSync,
  access,
  accessSync,
  chown,
  chownSync,
  chmod,
  chmodSync,
  close,
  closeSync,
  copyFile,
  copyFileSync,
  cp,
  cpSync,
  createReadStream,
  createWriteStream,
  exists,
  existsSync,
  fchown,
  fchownSync,
  fchmod,
  fchmodSync,
  fdatasync,
  fdatasyncSync,
  fstat,
  fstatSync,
  fsync,
  fsyncSync,
  ftruncate,
  ftruncateSync,
  futimes,
  futimesSync,
  glob,
  globSync,
  lchown,
  lchownSync,
  lchmod: constants.O_SYMLINK !== undefined ? lchmod : undefined,
  lchmodSync: constants.O_SYMLINK !== undefined ? lchmodSync : undefined,
  link,
  linkSync,
  lstat,
  lstatSync,
  lutimes,
  lutimesSync,
  mkdir,
  mkdirSync,
  mkdtemp,
  mkdtempSync,
  mkdtempDisposableSync,
  mkstemp,
  mkstempSync,
  open,
  openSync,
  openAsBlob,
  openAsBlobSync,
  readdir,
  readdirSync,
  read,
  readSync,
  readv,
  readvSync,
  readFile,
  readFileSync,
  readlink,
  readlinkSync,
  realpath,
  realpathSync,
  rename,
  renameSync,
  rm,
  rmSync,
  rmdir,
  rmdirSync,
  stat,
  statfs,
  statSync,
  statfsSync,
  symlink,
  symlinkSync,
  truncate,
  truncateSync,
  unwatchFile,
  unlink,
  unlinkSync,
  utimes,
  utimesSync,
  watch,
  watchFile,
  writeFile,
  writeFileSync,
  write,
  writeSync,
  writev,
  writevSync,
  Dirent,
  Stats,

  get ReadStream() {
    lazyLoadStreams();
    return ReadStream;
  },

  set ReadStream(val) {
    ReadStream = val;
  },

  get WriteStream() {
    lazyLoadStreams();
    return WriteStream;
  },

  set WriteStream(val) {
    WriteStream = val;
  },

  // Legacy names... these have to be separate because of how graceful-fs
  // (and possibly other) modules monkey patch the values.
  get FileReadStream() {
    lazyLoadStreams();
    return FileReadStream;
  },

  set FileReadStream(val) {
    FileReadStream = val;
  },

  get FileWriteStream() {
    lazyLoadStreams();
    return FileWriteStream;
  },

  set FileWriteStream(val) {
    FileWriteStream = val;
  },

  get Utf8Stream() {
    lazyLoadUtf8Stream();
    return Utf8Stream;
  },

  // For tests
  _toUnixTimestamp: toUnixTimestamp,
};

defineLazyProperties(
  fs,
  'internal/fs/dir',
  ['Dir', 'opendir', 'opendirSync'],
);

ObjectDefineProperties(fs, {
  constants: {
    __proto__: null,
    configurable: false,
    enumerable: true,
    value: constants,
  },
  promises: {
    __proto__: null,
    configurable: true,
    enumerable: true,
    get() {
      promises ??= require('internal/fs/promises').exports;
      return promises;
    },
  },
});