Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,244 @@
'use strict';
// This file is a modified version of the fs-extra's copySync method.
const fsBinding = internalBinding('fs');
const { isSrcSubdir } = require('internal/fs/cp/cp');
const { codes: {
ERR_FS_CP_EEXIST,
ERR_FS_CP_EINVAL,
ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
ERR_INVALID_RETURN_VALUE,
} } = require('internal/errors');
const {
os: {
errno: {
EEXIST,
EINVAL,
},
},
} = internalBinding('constants');
const {
chmodSync,
copyFileSync,
lstatSync,
mkdirSync,
opendirSync,
readlinkSync,
statSync,
symlinkSync,
unlinkSync,
utimesSync,
} = require('fs');
const {
dirname,
isAbsolute,
join,
resolve,
} = require('path');
const { isPromise } = require('util/types');
function cpSyncFn(src, dest, opts) {
// Warn about using preserveTimestamps on 32-bit node
if (opts.preserveTimestamps && process.arch === 'ia32') {
const warning = 'Using the preserveTimestamps option in 32-bit ' +
'node is not recommended';
process.emitWarning(warning, 'TimestampPrecisionWarning');
}
if (opts.filter) {
const shouldCopy = opts.filter(src, dest);
if (isPromise(shouldCopy)) {
throw new ERR_INVALID_RETURN_VALUE('boolean', 'filter', shouldCopy);
}
if (!shouldCopy) return;
}
fsBinding.cpSyncCheckPaths(src, dest, opts.dereference, opts.recursive);
return getStats(src, dest, opts);
}
function getStats(src, dest, opts) {
// TODO(@anonrig): Avoid making two stat calls.
const statSyncFn = opts.dereference ? statSync : lstatSync;
const srcStat = statSyncFn(src);
const destStat = statSyncFn(dest, { bigint: true, throwIfNoEntry: false });
if (srcStat.isDirectory() && opts.recursive) {
return onDir(srcStat, destStat, src, dest, opts);
} else if (srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
return onLink(destStat, src, dest, opts.verbatimSymlinks);
}
// It is not possible to get here because all possible cases are handled above.
const assert = require('internal/assert');
assert.fail('Unreachable code');
}
function onFile(srcStat, destStat, src, dest, opts) {
if (!destStat) return copyFile(srcStat, src, dest, opts);
if (opts.force) {
return fsBinding.cpSyncOverrideFile(src, dest, opts.mode, opts.preserveTimestamps);
}
if (opts.errorOnExist) {
throw new ERR_FS_CP_EEXIST({
message: `${dest} already exists`,
path: dest,
syscall: 'cp',
errno: EEXIST,
code: 'EEXIST',
});
}
}
function copyFile(srcStat, src, dest, opts) {
copyFileSync(src, dest, opts.mode);
if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest);
return setDestMode(dest, srcStat.mode);
}
function handleTimestamps(srcMode, src, dest) {
// Make sure the file is writable before setting the timestamp
// otherwise open fails with EPERM when invoked with 'r+'
// (through utimes call)
if (fileIsNotWritable(srcMode)) makeFileWritable(dest, srcMode);
return setDestTimestamps(src, dest);
}
function fileIsNotWritable(srcMode) {
return (srcMode & 0o200) === 0;
}
function makeFileWritable(dest, srcMode) {
return setDestMode(dest, srcMode | 0o200);
}
function setDestMode(dest, srcMode) {
return chmodSync(dest, srcMode);
}
function setDestTimestamps(src, dest) {
// The initial srcStat.atime cannot be trusted
// because it is modified by the read(2) system call
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
const updatedSrcStat = statSync(src);
return utimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime);
}
// TODO(@anonrig): Move this function to C++.
function onDir(srcStat, destStat, src, dest, opts) {
if (!destStat) return copyDir(src, dest, opts, true, srcStat.mode);
return copyDir(src, dest, opts);
}
function copyDir(src, dest, opts, mkDir, srcMode) {
if (!opts.filter) {
// The caller didn't provide a js filter function, in this case
// we can run the whole function faster in C++
// TODO(dario-piotrowicz): look into making cpSyncCopyDir also accept the potential filter function
return fsBinding.cpSyncCopyDir(src, dest,
opts.force,
opts.dereference,
opts.errorOnExist,
opts.verbatimSymlinks,
opts.preserveTimestamps);
}
if (mkDir) {
mkdirSync(dest);
}
const dir = opendirSync(src);
try {
let dirent;
while ((dirent = dir.readSync()) !== null) {
const { name } = dirent;
const srcItem = join(src, name);
const destItem = join(dest, name);
let shouldCopy = true;
if (opts.filter) {
shouldCopy = opts.filter(srcItem, destItem);
if (isPromise(shouldCopy)) {
throw new ERR_INVALID_RETURN_VALUE('boolean', 'filter', shouldCopy);
}
}
if (shouldCopy) {
getStats(srcItem, destItem, opts);
}
}
} finally {
dir.closeSync();
if (srcMode !== undefined) {
setDestMode(dest, srcMode);
}
}
}
// TODO(@anonrig): Move this function to C++.
function onLink(destStat, src, dest, verbatimSymlinks) {
let resolvedSrc = readlinkSync(src);
if (!verbatimSymlinks && !isAbsolute(resolvedSrc)) {
resolvedSrc = resolve(dirname(src), resolvedSrc);
}
if (!destStat) {
return symlinkSync(resolvedSrc, dest);
}
let resolvedDest;
try {
resolvedDest = readlinkSync(dest);
} catch (err) {
// Dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
// fs throws error anyway, so no need to guard against it here.
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') {
return symlinkSync(resolvedSrc, dest);
}
throw err;
}
if (!isAbsolute(resolvedDest)) {
resolvedDest = resolve(dirname(dest), resolvedDest);
}
const srcIsDir = fsBinding.internalModuleStat(src) === 1;
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) {
throw new ERR_FS_CP_EINVAL({
message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
`${resolvedDest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
// Prevent copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
return copyLink(resolvedSrc, dest);
}
function copyLink(resolvedSrc, dest) {
unlinkSync(dest);
return symlinkSync(resolvedSrc, dest);
}
module.exports = { cpSyncFn };

386
lib/internal/fs/cp/cp.js Normal file
View File

@ -0,0 +1,386 @@
'use strict';
// This file is a modified version of the fs-extra's copy method.
const {
ArrayPrototypeEvery,
ArrayPrototypeFilter,
Boolean,
PromisePrototypeThen,
PromiseReject,
SafePromiseAll,
StringPrototypeSplit,
} = primordials;
const {
codes: {
ERR_FS_CP_DIR_TO_NON_DIR,
ERR_FS_CP_EEXIST,
ERR_FS_CP_EINVAL,
ERR_FS_CP_FIFO_PIPE,
ERR_FS_CP_NON_DIR_TO_DIR,
ERR_FS_CP_SOCKET,
ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
ERR_FS_CP_UNKNOWN,
ERR_FS_EISDIR,
},
} = require('internal/errors');
const {
os: {
errno: {
EEXIST,
EISDIR,
EINVAL,
ENOTDIR,
},
},
} = internalBinding('constants');
const {
chmod,
copyFile,
lstat,
mkdir,
opendir,
readlink,
stat,
symlink,
unlink,
utimes,
} = require('fs/promises');
const {
dirname,
isAbsolute,
join,
parse,
resolve,
sep,
} = require('path');
const fsBinding = internalBinding('fs');
async function cpFn(src, dest, opts) {
// Warn about using preserveTimestamps on 32-bit node
if (opts.preserveTimestamps && process.arch === 'ia32') {
const warning = 'Using the preserveTimestamps option in 32-bit ' +
'node is not recommended';
process.emitWarning(warning, 'TimestampPrecisionWarning');
}
const stats = await checkPaths(src, dest, opts);
const { srcStat, destStat, skipped } = stats;
if (skipped) return;
await checkParentPaths(src, srcStat, dest);
return checkParentDir(destStat, src, dest, opts);
}
async function checkPaths(src, dest, opts) {
if (opts.filter && !(await opts.filter(src, dest))) {
return { __proto__: null, skipped: true };
}
const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts);
if (destStat) {
if (areIdentical(srcStat, destStat)) {
throw new ERR_FS_CP_EINVAL({
message: 'src and dest cannot be the same',
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
if (srcStat.isDirectory() && !destStat.isDirectory()) {
throw new ERR_FS_CP_DIR_TO_NON_DIR({
message: `cannot overwrite non-directory ${dest} ` +
`with directory ${src}`,
path: dest,
syscall: 'cp',
errno: EISDIR,
code: 'EISDIR',
});
}
if (!srcStat.isDirectory() && destStat.isDirectory()) {
throw new ERR_FS_CP_NON_DIR_TO_DIR({
message: `cannot overwrite directory ${dest} ` +
`with non-directory ${src}`,
path: dest,
syscall: 'cp',
errno: ENOTDIR,
code: 'ENOTDIR',
});
}
}
if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
throw new ERR_FS_CP_EINVAL({
message: `cannot copy ${src} to a subdirectory of self ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
return { __proto__: null, srcStat, destStat, skipped: false };
}
function areIdentical(srcStat, destStat) {
return destStat.ino && destStat.dev && destStat.ino === srcStat.ino &&
destStat.dev === srcStat.dev;
}
function getStats(src, dest, opts) {
const statFunc = opts.dereference ?
(file) => stat(file, { bigint: true }) :
(file) => lstat(file, { bigint: true });
return SafePromiseAll([
statFunc(src),
PromisePrototypeThen(statFunc(dest), undefined, (err) => {
if (err.code === 'ENOENT') return null;
throw err;
}),
]);
}
async function checkParentDir(destStat, src, dest, opts) {
const destParent = dirname(dest);
const dirExists = await pathExists(destParent);
if (dirExists) return getStatsForCopy(destStat, src, dest, opts);
await mkdir(destParent, { recursive: true });
return getStatsForCopy(destStat, src, dest, opts);
}
function pathExists(dest) {
return PromisePrototypeThen(
stat(dest),
() => true,
(err) => (err.code === 'ENOENT' ? false : PromiseReject(err)));
}
// Recursively check if dest parent is a subdirectory of src.
// It works for all file types including symlinks since it
// checks the src and dest inodes. It starts from the deepest
// parent and stops once it reaches the src parent or the root path.
async function checkParentPaths(src, srcStat, dest) {
const srcParent = resolve(dirname(src));
const destParent = resolve(dirname(dest));
if (destParent === srcParent || destParent === parse(destParent).root) {
return;
}
let destStat;
try {
destStat = await stat(destParent, { bigint: true });
} catch (err) {
if (err.code === 'ENOENT') return;
throw err;
}
if (areIdentical(srcStat, destStat)) {
throw new ERR_FS_CP_EINVAL({
message: `cannot copy ${src} to a subdirectory of self ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
return checkParentPaths(src, srcStat, destParent);
}
const normalizePathToArray = (path) =>
ArrayPrototypeFilter(StringPrototypeSplit(resolve(path), sep), Boolean);
// Return true if dest is a subdir of src, otherwise false.
// It only checks the path strings.
function isSrcSubdir(src, dest) {
const srcArr = normalizePathToArray(src);
const destArr = normalizePathToArray(dest);
return ArrayPrototypeEvery(srcArr, (cur, i) => destArr[i] === cur);
}
async function getStatsForCopy(destStat, src, dest, opts) {
const statFn = opts.dereference ? stat : lstat;
const srcStat = await statFn(src);
if (srcStat.isDirectory() && opts.recursive) {
return onDir(srcStat, destStat, src, dest, opts);
} else if (srcStat.isDirectory()) {
throw new ERR_FS_EISDIR({
message: `${src} is a directory (not copied)`,
path: src,
syscall: 'cp',
errno: EISDIR,
code: 'EISDIR',
});
} else if (srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
return onLink(destStat, src, dest, opts);
} else if (srcStat.isSocket()) {
throw new ERR_FS_CP_SOCKET({
message: `cannot copy a socket file: ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
} else if (srcStat.isFIFO()) {
throw new ERR_FS_CP_FIFO_PIPE({
message: `cannot copy a FIFO pipe: ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
throw new ERR_FS_CP_UNKNOWN({
message: `cannot copy an unknown file type: ${dest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
function onFile(srcStat, destStat, src, dest, opts) {
if (!destStat) return _copyFile(srcStat, src, dest, opts);
return mayCopyFile(srcStat, src, dest, opts);
}
async function mayCopyFile(srcStat, src, dest, opts) {
if (opts.force) {
await unlink(dest);
return _copyFile(srcStat, src, dest, opts);
} else if (opts.errorOnExist) {
throw new ERR_FS_CP_EEXIST({
message: `${dest} already exists`,
path: dest,
syscall: 'cp',
errno: EEXIST,
code: 'EEXIST',
});
}
}
async function _copyFile(srcStat, src, dest, opts) {
await copyFile(src, dest, opts.mode);
if (opts.preserveTimestamps) {
return handleTimestampsAndMode(srcStat.mode, src, dest);
}
return setDestMode(dest, srcStat.mode);
}
async function handleTimestampsAndMode(srcMode, src, dest) {
// Make sure the file is writable before setting the timestamp
// otherwise open fails with EPERM when invoked with 'r+'
// (through utimes call)
if (fileIsNotWritable(srcMode)) {
await makeFileWritable(dest, srcMode);
}
return setDestTimestampsAndMode(srcMode, src, dest);
}
function fileIsNotWritable(srcMode) {
return (srcMode & 0o200) === 0;
}
function makeFileWritable(dest, srcMode) {
return setDestMode(dest, srcMode | 0o200);
}
async function setDestTimestampsAndMode(srcMode, src, dest) {
await setDestTimestamps(src, dest);
return setDestMode(dest, srcMode);
}
function setDestMode(dest, srcMode) {
return chmod(dest, srcMode);
}
async function setDestTimestamps(src, dest) {
// The initial srcStat.atime cannot be trusted
// because it is modified by the read(2) system call
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
const updatedSrcStat = await stat(src);
return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime);
}
function onDir(srcStat, destStat, src, dest, opts) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
return copyDir(src, dest, opts);
}
async function mkDirAndCopy(srcMode, src, dest, opts) {
await mkdir(dest);
await copyDir(src, dest, opts);
return setDestMode(dest, srcMode);
}
async function copyDir(src, dest, opts) {
const dir = await opendir(src);
for await (const { name } of dir) {
const srcItem = join(src, name);
const destItem = join(dest, name);
const { destStat, skipped } = await checkPaths(srcItem, destItem, opts);
if (!skipped) await getStatsForCopy(destStat, srcItem, destItem, opts);
}
}
async function onLink(destStat, src, dest, opts) {
let resolvedSrc = await readlink(src);
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
resolvedSrc = resolve(dirname(src), resolvedSrc);
}
if (!destStat) {
return symlink(resolvedSrc, dest);
}
let resolvedDest;
try {
resolvedDest = await readlink(dest);
} catch (err) {
// Dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
// fs throws error anyway, so no need to guard against it here.
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') {
return symlink(resolvedSrc, dest);
}
throw err;
}
if (!isAbsolute(resolvedDest)) {
resolvedDest = resolve(dirname(dest), resolvedDest);
}
const srcIsDir = fsBinding.internalModuleStat(src) === 1;
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) {
throw new ERR_FS_CP_EINVAL({
message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
`${resolvedDest}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
// Do not copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
const srcStat = await stat(src);
if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
path: dest,
syscall: 'cp',
errno: EINVAL,
code: 'EINVAL',
});
}
return copyLink(resolvedSrc, dest);
}
async function copyLink(resolvedSrc, dest) {
await unlink(dest);
return symlink(resolvedSrc, dest);
}
module.exports = {
areIdentical,
cpFn,
isSrcSubdir,
};

368
lib/internal/fs/dir.js Normal file
View File

@ -0,0 +1,368 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeShift,
FunctionPrototypeBind,
ObjectDefineProperties,
PromiseReject,
SymbolAsyncDispose,
SymbolAsyncIterator,
SymbolDispose,
} = primordials;
const pathModule = require('path');
const binding = internalBinding('fs');
const dirBinding = internalBinding('fs_dir');
const {
codes: {
ERR_DIR_CLOSED,
ERR_DIR_CONCURRENT_OPERATION,
ERR_MISSING_ARGS,
},
} = require('internal/errors');
const { FSReqCallback } = binding;
const {
assignFunctionName,
promisify,
} = require('internal/util');
const {
getDirent,
getOptions,
getValidatedPath,
} = require('internal/fs/utils');
const {
validateFunction,
validateUint32,
} = require('internal/validators');
class Dir {
#handle;
#path;
#bufferedEntries = [];
#closed = false;
#options;
#readPromisified;
#closePromisified;
#operationQueue = null;
#handlerQueue = [];
constructor(handle, path, options) {
if (handle == null) throw new ERR_MISSING_ARGS('handle');
this.#handle = handle;
this.#path = path;
this.#options = {
bufferSize: 32,
...getOptions(options, {
encoding: 'utf8',
}),
};
validateUint32(this.#options.bufferSize, 'options.bufferSize', true);
this.#readPromisified = FunctionPrototypeBind(
promisify(this.#readImpl), this, false);
this.#closePromisified = FunctionPrototypeBind(
promisify(this.close), this);
}
get path() {
return this.#path;
}
#processHandlerQueue() {
while (this.#handlerQueue.length > 0) {
const handler = ArrayPrototypeShift(this.#handlerQueue);
const { handle, path } = handler;
const result = handle.read(
this.#options.encoding,
this.#options.bufferSize,
);
if (result !== null) {
this.processReadResult(path, result);
if (result.length > 0) {
ArrayPrototypePush(this.#handlerQueue, handler);
}
} else {
handle.close();
}
if (this.#bufferedEntries.length > 0) {
break;
}
}
return this.#bufferedEntries.length > 0;
}
read(callback) {
return arguments.length === 0 ? this.#readPromisified() : this.#readImpl(true, callback);
}
#readImpl(maybeSync, callback) {
if (this.#closed === true) {
throw new ERR_DIR_CLOSED();
}
if (callback === undefined) {
return this.#readPromisified();
}
validateFunction(callback, 'callback');
if (this.#operationQueue !== null) {
ArrayPrototypePush(this.#operationQueue, () => {
this.#readImpl(maybeSync, callback);
});
return;
}
if (this.#processHandlerQueue()) {
try {
const dirent = ArrayPrototypeShift(this.#bufferedEntries);
if (this.#options.recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
if (maybeSync)
process.nextTick(callback, null, dirent);
else
callback(null, dirent);
return;
} catch (error) {
return callback(error);
}
}
const req = new FSReqCallback();
req.oncomplete = (err, result) => {
process.nextTick(() => {
const queue = this.#operationQueue;
this.#operationQueue = null;
for (const op of queue) op();
});
if (err || result === null) {
return callback(err, result);
}
try {
this.processReadResult(this.#path, result);
const dirent = ArrayPrototypeShift(this.#bufferedEntries);
if (this.#options.recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
callback(null, dirent);
} catch (error) {
callback(error);
}
};
this.#operationQueue = [];
this.#handle.read(
this.#options.encoding,
this.#options.bufferSize,
req,
);
}
processReadResult(path, result) {
for (let i = 0; i < result.length; i += 2) {
ArrayPrototypePush(
this.#bufferedEntries,
getDirent(
path,
result[i],
result[i + 1],
),
);
}
}
readSyncRecursive(dirent) {
const path = pathModule.join(dirent.parentPath, dirent.name);
const handle = dirBinding.opendir(
path,
this.#options.encoding,
);
if (handle === undefined) {
return;
}
ArrayPrototypePush(this.#handlerQueue, { handle, path });
}
readSync() {
if (this.#closed === true) {
throw new ERR_DIR_CLOSED();
}
if (this.#operationQueue !== null) {
throw new ERR_DIR_CONCURRENT_OPERATION();
}
if (this.#processHandlerQueue()) {
const dirent = ArrayPrototypeShift(this.#bufferedEntries);
if (this.#options.recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
return dirent;
}
const result = this.#handle.read(
this.#options.encoding,
this.#options.bufferSize,
);
if (result === null) {
return result;
}
this.processReadResult(this.#path, result);
const dirent = ArrayPrototypeShift(this.#bufferedEntries);
if (this.#options.recursive && dirent.isDirectory()) {
this.readSyncRecursive(dirent);
}
return dirent;
}
close(callback) {
if (callback === undefined) {
if (this.#closed === true) {
return PromiseReject(new ERR_DIR_CLOSED());
}
return this.#closePromisified();
}
validateFunction(callback, 'callback');
if (this.#closed === true) {
process.nextTick(callback, new ERR_DIR_CLOSED());
return;
}
if (this.#operationQueue !== null) {
ArrayPrototypePush(this.#operationQueue, () => {
this.close(callback);
});
return;
}
while (this.#handlerQueue.length > 0) {
const handler = ArrayPrototypeShift(this.#handlerQueue);
handler.handle.close();
}
this.#closed = true;
const req = new FSReqCallback();
req.oncomplete = callback;
this.#handle.close(req);
}
closeSync() {
if (this.#closed === true) {
throw new ERR_DIR_CLOSED();
}
if (this.#operationQueue !== null) {
throw new ERR_DIR_CONCURRENT_OPERATION();
}
while (this.#handlerQueue.length > 0) {
const handler = ArrayPrototypeShift(this.#handlerQueue);
handler.handle.close();
}
this.#closed = true;
this.#handle.close();
}
async* entries() {
try {
while (true) {
const result = await this.#readPromisified();
if (result === null) {
break;
}
yield result;
}
} finally {
await this.#closePromisified();
}
}
}
const nonEnumerableDescriptor = {
enumerable: false,
writable: true,
configurable: true,
};
ObjectDefineProperties(Dir.prototype, {
[SymbolDispose]: {
__proto__: null,
...nonEnumerableDescriptor,
value: assignFunctionName(SymbolDispose, function() {
this.closeSync();
}),
},
[SymbolAsyncDispose]: {
__proto__: null,
...nonEnumerableDescriptor,
value: assignFunctionName(SymbolAsyncDispose, function() {
this.close();
}),
},
[SymbolAsyncIterator]: {
__proto__: null,
...nonEnumerableDescriptor,
value: Dir.prototype.entries,
},
});
function opendir(path, options, callback) {
callback = typeof options === 'function' ? options : callback;
validateFunction(callback, 'callback');
path = getValidatedPath(path);
options = getOptions(options, {
encoding: 'utf8',
});
function opendirCallback(error, handle) {
if (error) {
callback(error);
} else {
callback(null, new Dir(handle, path, options));
}
}
const req = new FSReqCallback();
req.oncomplete = opendirCallback;
dirBinding.opendir(
path,
options.encoding,
req,
);
}
function opendirSync(path, options) {
path = getValidatedPath(path);
options = getOptions(options, { encoding: 'utf8' });
const handle = dirBinding.opendirSync(path);
return new Dir(handle, path, options);
}
module.exports = {
Dir,
opendir,
opendirSync,
};

786
lib/internal/fs/glob.js Normal file
View File

@ -0,0 +1,786 @@
'use strict';
const {
ArrayFrom,
ArrayIsArray,
ArrayPrototypeAt,
ArrayPrototypeFlatMap,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeSome,
Promise,
PromisePrototypeThen,
SafeMap,
SafeSet,
StringPrototypeEndsWith,
} = primordials;
const { lstatSync, readdirSync } = require('fs');
const { lstat, readdir } = require('fs/promises');
const { join, resolve, basename, isAbsolute, dirname } = require('path');
const {
kEmptyObject,
isWindows,
isMacOS,
} = require('internal/util');
const {
validateObject,
validateString,
validateStringArray,
} = require('internal/validators');
const { DirentFromStats } = require('internal/fs/utils');
const {
codes: {
ERR_INVALID_ARG_TYPE,
},
hideStackFrames,
} = require('internal/errors');
const assert = require('internal/assert');
const { toPathIfFileURL } = require('internal/url');
let minimatch;
function lazyMinimatch() {
minimatch ??= require('internal/deps/minimatch/index');
return minimatch;
}
/**
* @param {string} path
* @returns {Promise<DirentFromStats|null>}
*/
async function getDirent(path) {
let stat;
try {
stat = await lstat(path);
} catch {
return null;
}
return new DirentFromStats(basename(path), stat, dirname(path));
}
/**
* @param {string} path
* @returns {DirentFromStats|null}
*/
function getDirentSync(path) {
const stat = lstatSync(path, { throwIfNoEntry: false });
if (stat === undefined) {
return null;
}
return new DirentFromStats(basename(path), stat, dirname(path));
}
/**
* @callback validateStringArrayOrFunction
* @param {*} value
* @param {string} name
*/
const validateStringArrayOrFunction = hideStackFrames((value, name) => {
if (ArrayIsArray(value)) {
for (let i = 0; i < value.length; ++i) {
if (typeof value[i] !== 'string') {
throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]);
}
}
return;
}
if (typeof value !== 'function') {
throw new ERR_INVALID_ARG_TYPE(name, ['string[]', 'function'], value);
}
});
/**
* @param {string} pattern
* @param {options} options
* @returns {Minimatch}
*/
function createMatcher(pattern, options = kEmptyObject) {
const opts = {
__proto__: null,
nocase: isWindows || isMacOS,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: process.platform,
nocaseMagicOnly: true,
...options,
};
return new (lazyMinimatch().Minimatch)(pattern, opts);
}
class Cache {
#cache = new SafeMap();
#statsCache = new SafeMap();
#readdirCache = new SafeMap();
stat(path) {
const cached = this.#statsCache.get(path);
if (cached) {
return cached;
}
const promise = getDirent(path);
this.#statsCache.set(path, promise);
return promise;
}
statSync(path) {
const cached = this.#statsCache.get(path);
// Do not return a promise from a sync function.
if (cached && !(cached instanceof Promise)) {
return cached;
}
const val = getDirentSync(path);
this.#statsCache.set(path, val);
return val;
}
addToStatCache(path, val) {
this.#statsCache.set(path, val);
}
async readdir(path) {
const cached = this.#readdirCache.get(path);
if (cached) {
return cached;
}
const promise = PromisePrototypeThen(readdir(path, { __proto__: null, withFileTypes: true }), null, () => null);
this.#readdirCache.set(path, promise);
return promise;
}
readdirSync(path) {
const cached = this.#readdirCache.get(path);
if (cached) {
return cached;
}
let val;
try {
val = readdirSync(path, { __proto__: null, withFileTypes: true });
} catch {
val = [];
}
this.#readdirCache.set(path, val);
return val;
}
add(path, pattern) {
let cache = this.#cache.get(path);
if (!cache) {
cache = new SafeSet();
this.#cache.set(path, cache);
}
const originalSize = cache.size;
pattern.indexes.forEach((index) => cache.add(pattern.cacheKey(index)));
return cache.size !== originalSize + pattern.indexes.size;
}
seen(path, pattern, index) {
return this.#cache.get(path)?.has(pattern.cacheKey(index));
}
}
class Pattern {
#pattern;
#globStrings;
indexes;
symlinks;
last;
constructor(pattern, globStrings, indexes, symlinks) {
this.#pattern = pattern;
this.#globStrings = globStrings;
this.indexes = indexes;
this.symlinks = symlinks;
this.last = pattern.length - 1;
}
isLast(isDirectory) {
return this.indexes.has(this.last) ||
(this.at(-1) === '' && isDirectory &&
this.indexes.has(this.last - 1) && this.at(-2) === lazyMinimatch().GLOBSTAR);
}
isFirst() {
return this.indexes.has(0);
}
get hasSeenSymlinks() {
return ArrayPrototypeSome(ArrayFrom(this.indexes), (i) => !this.symlinks.has(i));
}
at(index) {
return ArrayPrototypeAt(this.#pattern, index);
}
child(indexes, symlinks = new SafeSet()) {
return new Pattern(this.#pattern, this.#globStrings, indexes, symlinks);
}
test(index, path) {
if (index > this.#pattern.length) {
return false;
}
const pattern = this.#pattern[index];
if (pattern === lazyMinimatch().GLOBSTAR) {
return true;
}
if (typeof pattern === 'string') {
return pattern === path;
}
if (typeof pattern?.test === 'function') {
return pattern.test(path);
}
return false;
}
cacheKey(index) {
let key = '';
for (let i = index; i < this.#globStrings.length; i++) {
key += this.#globStrings[i];
if (i !== this.#globStrings.length - 1) {
key += '/';
}
}
return key;
}
}
class ResultSet extends SafeSet {
#root = '.';
#isExcluded = () => false;
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
setup(root, isExcludedFn) {
this.#root = root;
this.#isExcluded = isExcludedFn;
}
add(value) {
if (this.#isExcluded(resolve(this.#root, value))) {
return false;
}
super.add(value);
return true;
}
}
class Glob {
#root;
#exclude;
#cache = new Cache();
#results = new ResultSet();
#queue = [];
#subpatterns = new SafeMap();
#patterns;
#withFileTypes;
#isExcluded = () => false;
constructor(pattern, options = kEmptyObject) {
validateObject(options, 'options');
const { exclude, cwd, withFileTypes } = options;
this.#root = toPathIfFileURL(cwd) ?? '.';
this.#withFileTypes = !!withFileTypes;
if (exclude != null) {
validateStringArrayOrFunction(exclude, 'options.exclude');
if (ArrayIsArray(exclude)) {
assert(typeof this.#root === 'string');
// Convert the path part of exclude patterns to absolute paths for
// consistent comparison before instantiating matchers.
const matchers = exclude
.map((pattern) => resolve(this.#root, pattern))
.map((pattern) => createMatcher(pattern));
this.#isExcluded = (value) =>
matchers.some((matcher) => matcher.match(value));
this.#results.setup(this.#root, this.#isExcluded);
} else {
this.#exclude = exclude;
}
}
let patterns;
if (typeof pattern === 'object') {
validateStringArray(pattern, 'patterns');
patterns = pattern;
} else {
validateString(pattern, 'patterns');
patterns = [pattern];
}
this.matchers = ArrayPrototypeMap(patterns, (pattern) => createMatcher(pattern));
this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set,
(pattern, i) => new Pattern(
pattern,
matcher.globParts[i],
new SafeSet().add(0),
new SafeSet(),
)));
}
globSync() {
ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns });
while (this.#queue.length > 0) {
const item = ArrayPrototypePop(this.#queue);
for (let i = 0; i < item.patterns.length; i++) {
this.#addSubpatterns(item.path, item.patterns[i]);
}
this.#subpatterns
.forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }));
this.#subpatterns.clear();
}
return ArrayFrom(
this.#results,
this.#withFileTypes ? (path) => this.#cache.statSync(
isAbsolute(path) ?
path :
join(this.#root, path),
) : undefined,
);
}
#addSubpattern(path, pattern) {
if (this.#isExcluded(path)) {
return;
}
const fullpath = resolve(this.#root, path);
// If path is a directory, add trailing slash and test patterns again.
if (this.#isExcluded(`${fullpath}/`) && this.#cache.statSync(fullpath).isDirectory()) {
return;
}
if (this.#exclude) {
if (this.#withFileTypes) {
const stat = this.#cache.statSync(path);
if (stat !== null) {
if (this.#exclude(stat)) {
return;
}
}
} else if (this.#exclude(path)) {
return;
}
}
if (!this.#subpatterns.has(path)) {
this.#subpatterns.set(path, [pattern]);
} else {
ArrayPrototypePush(this.#subpatterns.get(path), pattern);
}
}
#addSubpatterns(path, pattern) {
const seen = this.#cache.add(path, pattern);
if (seen) {
return;
}
const fullpath = resolve(this.#root, path);
const stat = this.#cache.statSync(fullpath);
const last = pattern.last;
const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks);
const isLast = pattern.isLast(isDirectory);
const isFirst = pattern.isFirst();
if (this.#isExcluded(fullpath)) {
return;
}
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
// Absolute path, go to root
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '') {
// Absolute path, go to root
this.#addSubpattern('/', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '..') {
// Start with .., go to parent
this.#addSubpattern('../', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '.') {
// Start with ., proceed
this.#addSubpattern('.', pattern.child(new SafeSet().add(1)));
return;
}
if (isLast && typeof pattern.at(-1) === 'string') {
// Add result if it exists
const p = pattern.at(-1);
const stat = this.#cache.statSync(join(fullpath, p));
if (stat && (p || isDirectory)) {
this.#results.add(join(path, p));
}
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
return;
}
} else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR &&
(path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) {
// If pattern ends with **, add to results
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
this.#results.add(path);
}
if (!isDirectory) {
return;
}
let children;
const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value);
if (typeof firstPattern === 'string') {
const stat = this.#cache.statSync(join(fullpath, firstPattern));
if (stat) {
stat.name = firstPattern;
children = [stat];
} else {
return;
}
} else {
children = this.#cache.readdirSync(fullpath);
}
for (let i = 0; i < children.length; i++) {
const entry = children[i];
const entryPath = join(path, entry.name);
this.#cache.addToStatCache(join(fullpath, entry.name), entry);
const subPatterns = new SafeSet();
const nSymlinks = new SafeSet();
for (const index of pattern.indexes) {
// For each child, check potential patterns
if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) {
return;
}
const current = pattern.at(index);
const nextIndex = index + 1;
const next = pattern.at(nextIndex);
const fromSymlink = pattern.symlinks.has(index);
if (current === lazyMinimatch().GLOBSTAR) {
if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
continue;
}
if (!fromSymlink && entry.isDirectory()) {
// If directory, add ** to its potential patterns
subPatterns.add(index);
} else if (!fromSymlink && index === last) {
// If ** is last, add to results
this.#results.add(entryPath);
}
// Any pattern after ** is also a potential pattern
// so we can already test it here
const nextMatches = pattern.test(nextIndex, entry.name);
if (nextMatches && nextIndex === last && !isLast) {
// If next pattern is the last one, add to results
this.#results.add(entryPath);
} else if (nextMatches && entry.isDirectory()) {
// Pattern matched, meaning two patterns forward
// are also potential patterns
// e.g **/b/c when entry is a/b - add c to potential patterns
subPatterns.add(index + 2);
}
if ((nextMatches || pattern.at(0) === '.') &&
(entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) {
// If pattern after ** matches, or pattern starts with "."
// and entry is a directory or symlink, add to potential patterns
subPatterns.add(nextIndex);
}
if (entry.isSymbolicLink()) {
nSymlinks.add(index);
}
if (next === '..' && entry.isDirectory()) {
// In case pattern is "**/..",
// both parent and current directory should be added to the queue
// if this is the last pattern, add to results instead
const parent = join(path, '..');
if (nextIndex < last) {
if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) {
this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) {
this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
} else {
if (!this.#cache.seen(path, pattern, nextIndex)) {
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
this.#results.add(path);
}
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
this.#results.add(parent);
}
}
}
}
if (typeof current === 'string') {
if (pattern.test(index, entry.name) && index !== last) {
// If current pattern matches entry name
// the next pattern is a potential pattern
subPatterns.add(nextIndex);
} else if (current === '.' && pattern.test(nextIndex, entry.name)) {
// If current pattern is ".", proceed to test next pattern
if (nextIndex === last) {
this.#results.add(entryPath);
} else {
subPatterns.add(nextIndex + 1);
}
}
}
if (typeof current === 'object' && pattern.test(index, entry.name)) {
// If current pattern is a regex that matches entry name (e.g *.js)
// add next pattern to potential patterns, or to results if it's the last pattern
if (index === last) {
this.#results.add(entryPath);
} else if (entry.isDirectory()) {
subPatterns.add(nextIndex);
}
}
}
if (subPatterns.size > 0) {
// If there are potential patterns, add to queue
this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks));
}
}
}
async* glob() {
ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns });
while (this.#queue.length > 0) {
const item = ArrayPrototypePop(this.#queue);
for (let i = 0; i < item.patterns.length; i++) {
yield* this.#iterateSubpatterns(item.path, item.patterns[i]);
}
this.#subpatterns
.forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }));
this.#subpatterns.clear();
}
}
async* #iterateSubpatterns(path, pattern) {
const seen = this.#cache.add(path, pattern);
if (seen) {
return;
}
const fullpath = resolve(this.#root, path);
const stat = await this.#cache.stat(fullpath);
const last = pattern.last;
const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks);
const isLast = pattern.isLast(isDirectory);
const isFirst = pattern.isFirst();
if (this.#isExcluded(fullpath)) {
return;
}
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
// Absolute path, go to root
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '') {
// Absolute path, go to root
this.#addSubpattern('/', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '..') {
// Start with .., go to parent
this.#addSubpattern('../', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '.') {
// Start with ., proceed
this.#addSubpattern('.', pattern.child(new SafeSet().add(1)));
return;
}
if (isLast && typeof pattern.at(-1) === 'string') {
// Add result if it exists
const p = pattern.at(-1);
const stat = await this.#cache.stat(join(fullpath, p));
if (stat && (p || isDirectory)) {
const result = join(path, p);
if (!this.#results.has(result)) {
if (this.#results.add(result)) {
yield this.#withFileTypes ? stat : result;
}
}
}
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
return;
}
} else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR &&
(path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) {
// If pattern ends with **, add to results
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
if (!this.#results.has(path)) {
if (this.#results.add(path)) {
yield this.#withFileTypes ? stat : path;
}
}
}
if (!isDirectory) {
return;
}
let children;
const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value);
if (typeof firstPattern === 'string') {
const stat = await this.#cache.stat(join(fullpath, firstPattern));
if (stat) {
stat.name = firstPattern;
children = [stat];
} else {
return;
}
} else {
children = await this.#cache.readdir(fullpath);
}
for (let i = 0; i < children.length; i++) {
const entry = children[i];
const entryPath = join(path, entry.name);
this.#cache.addToStatCache(join(fullpath, entry.name), entry);
const subPatterns = new SafeSet();
const nSymlinks = new SafeSet();
for (const index of pattern.indexes) {
// For each child, check potential patterns
if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) {
return;
}
const current = pattern.at(index);
const nextIndex = index + 1;
const next = pattern.at(nextIndex);
const fromSymlink = pattern.symlinks.has(index);
if (current === lazyMinimatch().GLOBSTAR) {
if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
continue;
}
if (!fromSymlink && entry.isDirectory()) {
// If directory, add ** to its potential patterns
subPatterns.add(index);
} else if (!fromSymlink && index === last) {
// If ** is last, add to results
if (!this.#results.has(entryPath)) {
if (this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
}
// Any pattern after ** is also a potential pattern
// so we can already test it here
const nextMatches = pattern.test(nextIndex, entry.name);
if (nextMatches && nextIndex === last && !isLast) {
// If next pattern is the last one, add to results
if (!this.#results.has(entryPath)) {
if (this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
} else if (nextMatches && entry.isDirectory()) {
// Pattern matched, meaning two patterns forward
// are also potential patterns
// e.g **/b/c when entry is a/b - add c to potential patterns
subPatterns.add(index + 2);
}
if ((nextMatches || pattern.at(0) === '.') &&
(entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) {
// If pattern after ** matches, or pattern starts with "."
// and entry is a directory or symlink, add to potential patterns
subPatterns.add(nextIndex);
}
if (entry.isSymbolicLink()) {
nSymlinks.add(index);
}
if (next === '..' && entry.isDirectory()) {
// In case pattern is "**/..",
// both parent and current directory should be added to the queue
// if this is the last pattern, add to results instead
const parent = join(path, '..');
if (nextIndex < last) {
if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) {
this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) {
this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
} else {
if (!this.#cache.seen(path, pattern, nextIndex)) {
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
if (!this.#results.has(path)) {
if (this.#results.add(path)) {
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
}
}
}
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
if (!this.#results.has(parent)) {
if (this.#results.add(parent)) {
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
}
}
}
}
}
}
if (typeof current === 'string') {
if (pattern.test(index, entry.name) && index !== last) {
// If current pattern matches entry name
// the next pattern is a potential pattern
subPatterns.add(nextIndex);
} else if (current === '.' && pattern.test(nextIndex, entry.name)) {
// If current pattern is ".", proceed to test next pattern
if (nextIndex === last) {
if (!this.#results.has(entryPath)) {
if (this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
} else {
subPatterns.add(nextIndex + 1);
}
}
}
if (typeof current === 'object' && pattern.test(index, entry.name)) {
// If current pattern is a regex that matches entry name (e.g *.js)
// add next pattern to potential patterns, or to results if it's the last pattern
if (index === last) {
if (!this.#results.has(entryPath)) {
if (this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
} else if (entry.isDirectory()) {
subPatterns.add(nextIndex);
}
}
}
if (subPatterns.size > 0) {
// If there are potential patterns, add to queue
this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks));
}
}
}
}
/**
* Check if a path matches a glob pattern
* @param {string} path the path to check
* @param {string} pattern the glob pattern to match
* @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows`
* @returns {boolean}
*/
function matchGlobPattern(path, pattern, windows = isWindows) {
validateString(path, 'path');
validateString(pattern, 'pattern');
return lazyMinimatch().minimatch(path, pattern, {
kEmptyObject,
nocase: isMacOS || isWindows,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: windows ? 'win32' : 'posix',
nocaseMagicOnly: true,
});
}
module.exports = {
__proto__: null,
Glob,
matchGlobPattern,
};

1313
lib/internal/fs/promises.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
'use strict';
const {
ArrayPrototypePush,
MathMin,
ReflectApply,
} = primordials;
const {
constants: {
kReadFileBufferLength,
kReadFileUnknownBufferLength,
},
} = require('internal/fs/utils');
const { Buffer } = require('buffer');
const { FSReqCallback, close, read } = internalBinding('fs');
const {
AbortError,
aggregateTwoErrors,
} = require('internal/errors');
function readFileAfterRead(err, bytesRead) {
const context = this.context;
if (err)
return context.close(err);
context.pos += bytesRead;
if (context.pos === context.size || bytesRead === 0) {
context.close();
} else {
if (context.size === 0) {
// Unknown size, just read until we don't get bytes.
const buffer = bytesRead === kReadFileUnknownBufferLength ?
context.buffer : context.buffer.slice(0, bytesRead);
ArrayPrototypePush(context.buffers, buffer);
}
context.read();
}
}
function readFileAfterClose(err) {
const context = this.context;
const callback = context.callback;
let buffer = null;
if (context.err || err)
return callback(aggregateTwoErrors(err, context.err));
try {
if (context.size === 0)
buffer = Buffer.concat(context.buffers, context.pos);
else if (context.pos < context.size)
buffer = context.buffer.slice(0, context.pos);
else
buffer = context.buffer;
if (context.encoding)
buffer = buffer.toString(context.encoding);
} catch (err) {
return callback(err);
}
callback(null, buffer);
}
class ReadFileContext {
constructor(callback, encoding) {
this.fd = undefined;
this.isUserFd = undefined;
this.size = 0;
this.callback = callback;
this.buffers = null;
this.buffer = null;
this.pos = 0;
this.encoding = encoding;
this.err = null;
this.signal = undefined;
}
read() {
let buffer;
let offset;
let length;
if (this.signal?.aborted) {
return this.close(
new AbortError(undefined, { cause: this.signal.reason }));
}
if (this.size === 0) {
buffer = Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength);
offset = 0;
length = kReadFileUnknownBufferLength;
this.buffer = buffer;
} else {
buffer = this.buffer;
offset = this.pos;
length = MathMin(kReadFileBufferLength, this.size - this.pos);
}
const req = new FSReqCallback();
req.oncomplete = readFileAfterRead;
req.context = this;
read(this.fd, buffer, offset, length, -1, req);
}
close(err) {
if (this.isUserFd) {
process.nextTick(function tick(context) {
ReflectApply(readFileAfterClose, { context }, [null]);
}, this);
return;
}
const req = new FSReqCallback();
req.oncomplete = readFileAfterClose;
req.context = this;
this.err = err;
close(this.fd, req);
}
}
module.exports = ReadFileContext;

View File

@ -0,0 +1,275 @@
'use strict';
const {
Promise,
SafeMap,
SafeSet,
StringPrototypeStartsWith,
SymbolAsyncIterator,
} = primordials;
const { EventEmitter } = require('events');
const assert = require('internal/assert');
const {
AbortError,
codes: {
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const { getValidatedPath } = require('internal/fs/utils');
const { kFSWatchStart, StatWatcher } = require('internal/fs/watchers');
const { kEmptyObject } = require('internal/util');
const { validateBoolean, validateAbortSignal } = require('internal/validators');
const {
basename: pathBasename,
join: pathJoin,
relative: pathRelative,
resolve: pathResolve,
} = require('path');
let internalSync;
function lazyLoadFsSync() {
internalSync ??= require('fs');
return internalSync;
}
let kResistStopPropagation;
class FSWatcher extends EventEmitter {
#options = null;
#closed = false;
#files = new SafeMap();
#watchers = new SafeMap();
#symbolicFiles = new SafeSet();
#rootPath = pathResolve();
#watchingFile = false;
constructor(options = kEmptyObject) {
super();
assert(typeof options === 'object');
const { persistent, recursive, signal, encoding } = options;
// TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support.
if (recursive != null) {
validateBoolean(recursive, 'options.recursive');
}
if (persistent != null) {
validateBoolean(persistent, 'options.persistent');
}
if (signal != null) {
validateAbortSignal(signal, 'options.signal');
}
if (encoding != null) {
// This is required since on macOS and Windows it throws ERR_INVALID_ARG_VALUE
if (typeof encoding !== 'string') {
throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
}
}
this.#options = { persistent, recursive, signal, encoding };
}
close() {
if (this.#closed) {
return;
}
this.#closed = true;
for (const file of this.#files.keys()) {
this.#watchers.get(file)?.close();
this.#watchers.delete(file);
}
this.#files.clear();
this.#symbolicFiles.clear();
this.emit('close');
}
#unwatchFiles(file) {
this.#symbolicFiles.delete(file);
for (const filename of this.#files.keys()) {
if (StringPrototypeStartsWith(filename, file)) {
this.#files.delete(filename);
this.#watchers.get(filename)?.close();
this.#watchers.delete(filename);
}
}
}
#watchFolder(folder) {
const { readdirSync } = lazyLoadFsSync();
try {
const files = readdirSync(folder, {
withFileTypes: true,
});
for (const file of files) {
if (this.#closed) {
break;
}
const f = pathJoin(folder, file.name);
if (!this.#files.has(f)) {
this.emit('change', 'rename', pathRelative(this.#rootPath, f));
if (file.isSymbolicLink()) {
this.#symbolicFiles.add(f);
}
try {
this.#watchFile(f);
if (file.isDirectory() && !file.isSymbolicLink()) {
this.#watchFolder(f);
}
} catch (err) {
// Ignore ENOENT
if (err.code !== 'ENOENT') {
throw err;
}
}
}
}
} catch (error) {
this.emit('error', error);
}
}
#watchFile(file) {
if (this.#closed) {
return;
}
const { watch, statSync } = lazyLoadFsSync();
if (this.#files.has(file)) {
return;
}
{
const existingStat = statSync(file);
this.#files.set(file, existingStat);
}
const watcher = watch(file, {
persistent: this.#options.persistent,
}, (eventType, filename) => {
const existingStat = this.#files.get(file);
let currentStats;
try {
currentStats = statSync(file);
this.#files.set(file, currentStats);
} catch {
// This happens if the file was removed
}
if (currentStats === undefined || (currentStats.birthtimeMs === 0 && existingStat.birthtimeMs !== 0)) {
// The file is now deleted
this.#files.delete(file);
this.#watchers.delete(file);
watcher.close();
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
this.#unwatchFiles(file);
} else if (file === this.#rootPath && this.#watchingFile) {
// This case will only be triggered when watching a file with fs.watch
this.emit('change', 'change', pathBasename(file));
} else if (this.#symbolicFiles.has(file)) {
// Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
// Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
} else if (currentStats.isDirectory()) {
this.#watchFolder(file);
} else {
// Watching a directory will trigger a change event for child files)
this.emit('change', 'change', pathRelative(this.#rootPath, file));
}
});
this.#watchers.set(file, watcher);
}
[kFSWatchStart](filename) {
filename = pathResolve(getValidatedPath(filename));
try {
const file = lazyLoadFsSync().statSync(filename);
this.#rootPath = filename;
this.#closed = false;
this.#watchingFile = file.isFile();
this.#watchFile(filename);
if (file.isDirectory()) {
this.#watchFolder(filename);
}
} catch (error) {
if (error.code === 'ENOENT') {
error.filename = filename;
throw error;
}
}
}
ref() {
this.#files.forEach((file) => {
if (file instanceof StatWatcher) {
file.ref();
}
});
}
unref() {
this.#files.forEach((file) => {
if (file instanceof StatWatcher) {
file.unref();
}
});
}
[SymbolAsyncIterator]() {
const { signal } = this.#options;
const promiseExecutor = signal == null ?
(resolve) => {
this.once('change', (eventType, filename) => {
resolve({ __proto__: null, value: { eventType, filename } });
});
} : (resolve, reject) => {
const onAbort = () => {
this.close();
reject(new AbortError(undefined, { cause: signal.reason }));
};
if (signal.aborted) return onAbort();
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
signal.addEventListener('abort', onAbort, { __proto__: null, once: true, [kResistStopPropagation]: true });
this.once('change', (eventType, filename) => {
signal.removeEventListener('abort', onAbort);
resolve({ __proto__: null, value: { eventType, filename } });
});
};
return {
next: () => (this.#closed ?
{ __proto__: null, done: true } :
new Promise(promiseExecutor)),
return: () => {
this.close();
return { __proto__: null, done: true };
},
[SymbolAsyncIterator]() { return this; },
};
}
}
module.exports = {
FSWatcher,
kFSWatchStart,
};

169
lib/internal/fs/rimraf.js Normal file
View File

@ -0,0 +1,169 @@
// This file is a modified version of the rimraf module on npm. It has been
// modified in the following ways:
// - Use of the assert module has been replaced with core's error system.
// - All code related to the glob dependency has been removed.
// - Bring your own custom fs module is not currently supported.
// - Some basic code cleanup.
'use strict';
const {
Promise,
SafeSet,
} = primordials;
const { Buffer } = require('buffer');
const fs = require('fs');
const {
chmod,
lstat,
readdir,
rmdir,
stat,
unlink,
} = fs;
const { sep } = require('path');
const { setTimeout } = require('timers');
const { isWindows } = require('internal/util');
const notEmptyErrorCodes = new SafeSet(['ENOTEMPTY', 'EEXIST', 'EPERM']);
const retryErrorCodes = new SafeSet(
['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']);
const epermHandler = isWindows ? fixWinEPERM : _rmdir;
const readdirEncoding = 'buffer';
const separator = Buffer.from(sep);
function rimraf(path, options, callback) {
let retries = 0;
_rimraf(path, options, function CB(err) {
if (err) {
if (retryErrorCodes.has(err.code) && retries < options.maxRetries) {
retries++;
const delay = retries * options.retryDelay;
return setTimeout(_rimraf, delay, path, options, CB);
}
// The file is already gone.
if (err.code === 'ENOENT')
err = null;
}
callback(err);
});
}
function _rimraf(path, options, callback) {
// SunOS lets the root user unlink directories. Use lstat here to make sure
// it's not a directory.
lstat(path, (err, stats) => {
if (err) {
if (err.code === 'ENOENT')
return callback(null);
// Windows can EPERM on stat.
if (isWindows && err.code === 'EPERM')
return fixWinEPERM(path, options, err, callback);
} else if (stats.isDirectory()) {
return _rmdir(path, options, err, callback);
}
unlink(path, (err) => {
if (err) {
if (err.code === 'ENOENT')
return callback(null);
if (err.code === 'EISDIR')
return _rmdir(path, options, err, callback);
if (err.code === 'EPERM') {
return epermHandler(path, options, err, callback);
}
}
return callback(err);
});
});
}
function fixWinEPERM(path, options, originalErr, callback) {
chmod(path, 0o666, (err) => {
if (err)
return callback(err.code === 'ENOENT' ? null : originalErr);
stat(path, (err, stats) => {
if (err)
return callback(err.code === 'ENOENT' ? null : originalErr);
if (stats.isDirectory())
_rmdir(path, options, originalErr, callback);
else
unlink(path, callback);
});
});
}
function _rmdir(path, options, originalErr, callback) {
rmdir(path, (err) => {
if (err) {
if (notEmptyErrorCodes.has(err.code))
return _rmchildren(path, options, callback);
if (err.code === 'ENOTDIR')
return callback(originalErr);
}
callback(err);
});
}
function _rmchildren(path, options, callback) {
const pathBuf = Buffer.from(path);
readdir(pathBuf, readdirEncoding, (err, files) => {
if (err)
return callback(err);
let numFiles = files.length;
if (numFiles === 0)
return rmdir(path, callback);
let done = false;
const childPathPrefix = Buffer.concat([pathBuf, separator]);
for (let i = 0; i < files.length; i++) {
const childPath = Buffer.concat([childPathPrefix, files[i]]);
rimraf(childPath, options, (err) => {
if (done)
return;
if (err) {
done = true;
return callback(err);
}
numFiles--;
if (numFiles === 0)
rmdir(path, callback);
});
}
});
}
function rimrafPromises(path, options) {
return new Promise((resolve, reject) => {
rimraf(path, options, (err) => {
if (err)
return reject(err);
resolve();
});
});
}
module.exports = { rimraf, rimrafPromises };

566
lib/internal/fs/streams.js Normal file
View File

@ -0,0 +1,566 @@
'use strict';
const {
Array,
FunctionPrototypeBind,
MathMin,
ObjectDefineProperty,
ObjectSetPrototypeOf,
PromisePrototypeThen,
ReflectApply,
Symbol,
} = primordials;
const {
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
deprecate,
kEmptyObject,
} = require('internal/util');
const {
validateBoolean,
validateFunction,
validateInteger,
} = require('internal/validators');
const { errorOrDestroy } = require('internal/streams/destroy');
const fs = require('fs');
const { kRef, kUnref, FileHandle } = require('internal/fs/promises');
const { Buffer } = require('buffer');
const {
copyObject,
getOptions,
getValidatedFd,
validatePath,
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');
const kFs = Symbol('kFs');
const kHandle = Symbol('kHandle');
function _construct(callback) {
const stream = this;
if (typeof stream.fd === 'number') {
callback();
return;
}
if (stream.open !== openWriteFs && stream.open !== openReadFs) {
// Backwards compat for monkey patching open().
const orgEmit = stream.emit;
stream.emit = function(...args) {
if (args[0] === 'open') {
this.emit = orgEmit;
callback();
ReflectApply(orgEmit, this, args);
} else if (args[0] === 'error') {
this.emit = orgEmit;
callback(args[1]);
} else {
ReflectApply(orgEmit, this, args);
}
};
stream.open();
} else {
stream[kFs].open(stream.path, stream.flags, stream.mode, (er, fd) => {
if (er) {
callback(er);
} else {
stream.fd = fd;
callback();
stream.emit('open', stream.fd);
stream.emit('ready');
}
});
}
}
// This generates an fs operations structure for a FileHandle
const FileHandleOperations = (handle) => {
return {
open: (path, flags, mode, cb) => {
throw new ERR_METHOD_NOT_IMPLEMENTED('open()');
},
close: (fd, cb) => {
handle[kUnref]();
PromisePrototypeThen(handle.close(),
() => cb(), cb);
},
fsync: (fd, cb) => {
PromisePrototypeThen(handle.sync(), () => cb(), cb);
},
read: (fd, buf, offset, length, pos, cb) => {
PromisePrototypeThen(handle.read(buf, offset, length, pos),
(r) => cb(null, r.bytesRead, r.buffer),
(err) => cb(err, 0, buf));
},
write: (fd, buf, offset, length, pos, cb) => {
PromisePrototypeThen(handle.write(buf, offset, length, pos),
(r) => cb(null, r.bytesWritten, r.buffer),
(err) => cb(err, 0, buf));
},
writev: (fd, buffers, pos, cb) => {
PromisePrototypeThen(handle.writev(buffers, pos),
(r) => cb(null, r.bytesWritten, r.buffers),
(err) => cb(err, 0, buffers));
},
};
};
function close(stream, err, cb) {
if (!stream.fd) {
cb(err);
} else if (stream.flush) {
stream[kFs].fsync(stream.fd, (flushErr) => {
_close(stream, err || flushErr, cb);
});
} else {
_close(stream, err, cb);
}
}
function _close(stream, err, cb) {
stream[kFs].close(stream.fd, (er) => {
cb(er || err);
});
stream.fd = null;
}
function importFd(stream, options) {
if (typeof options.fd === 'number') {
// When fd is a raw descriptor, we must keep our fingers crossed
// that the descriptor won't get closed, or worse, replaced with
// another one
// https://github.com/nodejs/node/issues/35862
stream[kFs] = options.fs || fs;
return options.fd;
} else if (typeof options.fd === 'object' &&
options.fd instanceof FileHandle) {
// When fd is a FileHandle we can listen for 'close' events
if (options.fs) {
// FileHandle is not supported with custom fs operations
throw new ERR_METHOD_NOT_IMPLEMENTED('FileHandle with fs');
}
stream[kHandle] = options.fd;
stream[kFs] = FileHandleOperations(stream[kHandle]);
stream[kHandle][kRef]();
options.fd.on('close', FunctionPrototypeBind(stream.close, stream));
return options.fd.fd;
}
throw new ERR_INVALID_ARG_TYPE('options.fd',
['number', 'FileHandle'], options.fd);
}
function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
// A little bit bigger buffer and water marks by default
options = copyObject(getOptions(options, kEmptyObject));
if (options.highWaterMark === undefined)
options.highWaterMark = 64 * 1024;
if (options.autoDestroy === undefined) {
options.autoDestroy = false;
}
if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
// Path will be ignored when fd is specified, so it can be falsy
this.path = toPathIfFileURL(path);
this.flags = options.flags === undefined ? 'r' : options.flags;
this.mode = options.mode === undefined ? 0o666 : options.mode;
validatePath(this.path);
} else {
this.fd = getValidatedFd(importFd(this, options));
}
options.autoDestroy = options.autoClose === undefined ?
true : options.autoClose;
validateFunction(this[kFs].read, 'options.fs.read');
if (options.autoDestroy) {
validateFunction(this[kFs].close, 'options.fs.close');
}
this.start = options.start;
this.end = options.end;
this.pos = undefined;
this.bytesRead = 0;
this[kIsPerformingIO] = false;
if (this.start !== undefined) {
validateInteger(this.start, 'start', 0);
this.pos = this.start;
}
if (this.end === undefined) {
this.end = Infinity;
} else if (this.end !== Infinity) {
validateInteger(this.end, 'end', 0);
if (this.start !== undefined && this.start > this.end) {
throw new ERR_OUT_OF_RANGE(
'start',
`<= "end" (here: ${this.end})`,
this.start,
);
}
}
ReflectApply(Readable, this, [options]);
}
ObjectSetPrototypeOf(ReadStream.prototype, Readable.prototype);
ObjectSetPrototypeOf(ReadStream, Readable);
ObjectDefineProperty(ReadStream.prototype, 'autoClose', {
__proto__: null,
get() {
return this._readableState.autoDestroy;
},
set(val) {
this._readableState.autoDestroy = val;
},
});
const openReadFs = deprecate(function() {
// Noop.
}, 'ReadStream.prototype.open() is deprecated', 'DEP0135');
ReadStream.prototype.open = openReadFs;
ReadStream.prototype._construct = _construct;
ReadStream.prototype._read = function(n) {
n = this.pos !== undefined ?
MathMin(this.end - this.pos + 1, n) :
MathMin(this.end - this.bytesRead + 1, n);
if (n <= 0) {
this.push(null);
return;
}
const buf = Buffer.allocUnsafeSlow(n);
this[kIsPerformingIO] = true;
this[kFs]
.read(this.fd, buf, 0, n, this.pos, (er, bytesRead, buf) => {
this[kIsPerformingIO] = false;
// Tell ._destroy() that it's safe to close the fd now.
if (this.destroyed) {
this.emit(kIoDone, er);
return;
}
if (er) {
errorOrDestroy(this, er);
} else if (bytesRead > 0) {
if (this.pos !== undefined) {
this.pos += bytesRead;
}
this.bytesRead += bytesRead;
if (bytesRead !== buf.length) {
// Slow path. Shrink to fit.
// Copy instead of slice so that we don't retain
// large backing buffer for small reads.
const dst = Buffer.allocUnsafeSlow(bytesRead);
buf.copy(dst, 0, 0, bytesRead);
buf = dst;
}
this.push(buf);
} else {
this.push(null);
}
});
};
ReadStream.prototype._destroy = function(err, cb) {
// Usually for async IO it is safe to close a file descriptor
// even when there are pending operations. However, due to platform
// differences file IO is implemented using synchronous operations
// running in a thread pool. Therefore, file descriptors are not safe
// to close while used in a pending read or write operation. Wait for
// any pending IO (kIsPerformingIO) to complete (kIoDone).
if (this[kIsPerformingIO]) {
this.once(kIoDone, (er) => close(this, err || er, cb));
} else {
close(this, err, cb);
}
};
ReadStream.prototype.close = function(cb) {
if (typeof cb === 'function') finished(this, cb);
this.destroy();
};
ObjectDefineProperty(ReadStream.prototype, 'pending', {
__proto__: null,
get() { return this.fd === null; },
configurable: true,
});
function WriteStream(path, options) {
if (!(this instanceof WriteStream))
return new WriteStream(path, options);
options = copyObject(getOptions(options, kEmptyObject));
// Only buffers are supported.
options.decodeStrings = true;
if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
// Path will be ignored when fd is specified, so it can be falsy
this.path = toPathIfFileURL(path);
this.flags = options.flags === undefined ? 'w' : options.flags;
this.mode = options.mode === undefined ? 0o666 : options.mode;
validatePath(this.path);
} else {
this.fd = getValidatedFd(importFd(this, options));
}
options.autoDestroy = options.autoClose === undefined ?
true : options.autoClose;
if (!this[kFs].write && !this[kFs].writev) {
throw new ERR_INVALID_ARG_TYPE('options.fs.write', 'function',
this[kFs].write);
}
if (this[kFs].write) {
validateFunction(this[kFs].write, 'options.fs.write');
}
if (this[kFs].writev) {
validateFunction(this[kFs].writev, 'options.fs.writev');
}
if (options.autoDestroy) {
validateFunction(this[kFs].close, 'options.fs.close');
}
this.flush = options.flush;
if (this.flush == null) {
this.flush = false;
} else {
validateBoolean(this.flush, 'options.flush');
validateFunction(this[kFs].fsync, 'options.fs.fsync');
}
// It's enough to override either, in which case only one will be used.
if (!this[kFs].write) {
this._write = null;
}
if (!this[kFs].writev) {
this._writev = null;
}
this.start = options.start;
this.pos = undefined;
this.bytesWritten = 0;
this[kIsPerformingIO] = false;
if (this.start !== undefined) {
validateInteger(this.start, 'start', 0);
this.pos = this.start;
}
ReflectApply(Writable, this, [options]);
if (options.encoding)
this.setDefaultEncoding(options.encoding);
}
ObjectSetPrototypeOf(WriteStream.prototype, Writable.prototype);
ObjectSetPrototypeOf(WriteStream, Writable);
ObjectDefineProperty(WriteStream.prototype, 'autoClose', {
__proto__: null,
get() {
return this._writableState.autoDestroy;
},
set(val) {
this._writableState.autoDestroy = val;
},
});
const openWriteFs = deprecate(function() {
// Noop.
}, 'WriteStream.prototype.open() is deprecated', 'DEP0135');
WriteStream.prototype.open = openWriteFs;
WriteStream.prototype._construct = _construct;
function writeAll(data, size, pos, cb, retries = 0) {
this[kFs].write(this.fd, data, 0, size, pos, (er, bytesWritten, buffer) => {
// No data currently available and operation should be retried later.
if (er?.code === 'EAGAIN') {
er = null;
bytesWritten = 0;
}
if (this.destroyed || er) {
return cb(er || new ERR_STREAM_DESTROYED('write'));
}
this.bytesWritten += bytesWritten;
retries = bytesWritten ? 0 : retries + 1;
size -= bytesWritten;
pos += bytesWritten;
// Try writing non-zero number of bytes up to 5 times.
if (retries > 5) {
cb(new ERR_SYSTEM_ERROR('write failed'));
} else if (size) {
writeAll.call(this, buffer.slice(bytesWritten), size, pos, cb, retries);
} else {
cb();
}
});
}
function writevAll(chunks, size, pos, cb, retries = 0) {
this[kFs].writev(this.fd, chunks, this.pos, (er, bytesWritten, buffers) => {
// No data currently available and operation should be retried later.
if (er?.code === 'EAGAIN') {
er = null;
bytesWritten = 0;
}
if (this.destroyed || er) {
return cb(er || new ERR_STREAM_DESTROYED('writev'));
}
this.bytesWritten += bytesWritten;
retries = bytesWritten ? 0 : retries + 1;
size -= bytesWritten;
pos += bytesWritten;
// Try writing non-zero number of bytes up to 5 times.
if (retries > 5) {
cb(new ERR_SYSTEM_ERROR('writev failed'));
} else if (size) {
writevAll.call(this, [Buffer.concat(buffers).slice(bytesWritten)], size, pos, cb, retries);
} else {
cb();
}
});
}
WriteStream.prototype._write = function(data, encoding, cb) {
this[kIsPerformingIO] = true;
writeAll.call(this, data, data.length, this.pos, (er) => {
this[kIsPerformingIO] = false;
if (this.destroyed) {
// Tell ._destroy() that it's safe to close the fd now.
cb(er);
return this.emit(kIoDone, er);
}
cb(er);
});
if (this.pos !== undefined)
this.pos += data.length;
};
WriteStream.prototype._writev = function(data, cb) {
const len = data.length;
const chunks = new Array(len);
let size = 0;
for (let i = 0; i < len; i++) {
const chunk = data[i].chunk;
chunks[i] = chunk;
size += chunk.length;
}
this[kIsPerformingIO] = true;
writevAll.call(this, chunks, size, this.pos, (er) => {
this[kIsPerformingIO] = false;
if (this.destroyed) {
// Tell ._destroy() that it's safe to close the fd now.
cb(er);
return this.emit(kIoDone, er);
}
cb(er);
});
if (this.pos !== undefined)
this.pos += size;
};
WriteStream.prototype._destroy = function(err, cb) {
// Usually for async IO it is safe to close a file descriptor
// even when there are pending operations. However, due to platform
// differences file IO is implemented using synchronous operations
// running in a thread pool. Therefore, file descriptors are not safe
// to close while used in a pending read or write operation. Wait for
// any pending IO (kIsPerformingIO) to complete (kIoDone).
if (this[kIsPerformingIO]) {
this.once(kIoDone, (er) => close(this, err || er, cb));
} else {
close(this, err, cb);
}
};
WriteStream.prototype.close = function(cb) {
if (cb) {
if (this.closed) {
process.nextTick(cb);
return;
}
this.on('close', cb);
}
// If we are not autoClosing, we should call
// destroy on 'finish'.
if (!this.autoClose) {
this.on('finish', this.destroy);
}
// We use end() instead of destroy() because of
// https://github.com/nodejs/node/issues/2006
this.end();
};
// There is no shutdown() for files.
WriteStream.prototype.destroySoon = WriteStream.prototype.end;
ObjectDefineProperty(WriteStream.prototype, 'pending', {
__proto__: null,
get() { return this.fd === null; },
configurable: true,
});
module.exports = {
ReadStream,
WriteStream,
};

View File

@ -0,0 +1,49 @@
'use strict';
const {
ObjectSetPrototypeOf,
ReflectApply,
} = primordials;
const { kEmptyObject } = require('internal/util');
const { Writable } = require('stream');
const { closeSync, writeSync } = require('fs');
function SyncWriteStream(fd, options) {
ReflectApply(Writable, this, [{ autoDestroy: true }]);
options ||= kEmptyObject;
this.fd = fd;
this.readable = false;
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
}
ObjectSetPrototypeOf(SyncWriteStream.prototype, Writable.prototype);
ObjectSetPrototypeOf(SyncWriteStream, Writable);
SyncWriteStream.prototype._write = function(chunk, encoding, cb) {
try {
writeSync(this.fd, chunk, 0, chunk.length);
} catch (e) {
cb(e);
return;
}
cb();
};
SyncWriteStream.prototype._destroy = function(err, cb) {
if (this.fd === null) // already destroy()ed
return cb(err);
if (this.autoClose)
closeSync(this.fd);
this.fd = null;
cb(err);
};
SyncWriteStream.prototype.destroySoon =
SyncWriteStream.prototype.destroy;
module.exports = SyncWriteStream;

979
lib/internal/fs/utils.js Normal file
View File

@ -0,0 +1,979 @@
'use strict';
const {
ArrayIsArray,
BigInt,
Date,
DateNow,
DatePrototypeGetTime,
ErrorCaptureStackTrace,
FunctionPrototypeCall,
MathMin,
MathRound,
Number,
NumberIsFinite,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectIs,
ObjectSetPrototypeOf,
ReflectApply,
ReflectOwnKeys,
RegExpPrototypeSymbolReplace,
StringPrototypeEndsWith,
StringPrototypeIncludes,
Symbol,
TypedArrayPrototypeAt,
TypedArrayPrototypeIncludes,
} = primordials;
const { Buffer } = require('buffer');
const {
UVException,
codes: {
ERR_FS_EISDIR,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
},
hideStackFrames,
} = require('internal/errors');
const {
isArrayBufferView,
isBigInt64Array,
isDate,
isUint8Array,
} = require('internal/util/types');
const {
kEmptyObject,
once,
deprecate,
isWindows,
} = require('internal/util');
const { toPathIfFileURL } = require('internal/url');
const {
validateAbortSignal,
validateBoolean,
validateFunction,
validateInt32,
validateInteger,
validateObject,
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const kType = Symbol('type');
const kStats = Symbol('stats');
const assert = require('internal/assert');
const {
fs: {
F_OK = 0,
W_OK = 0,
R_OK = 0,
X_OK = 0,
COPYFILE_EXCL,
COPYFILE_FICLONE,
COPYFILE_FICLONE_FORCE,
O_APPEND,
O_CREAT,
O_EXCL,
O_RDONLY,
O_RDWR,
O_SYNC,
O_TRUNC,
O_WRONLY,
S_IFBLK,
S_IFCHR,
S_IFDIR,
S_IFIFO,
S_IFLNK,
S_IFMT,
S_IFREG,
S_IFSOCK,
UV_FS_SYMLINK_DIR,
UV_FS_SYMLINK_JUNCTION,
UV_DIRENT_UNKNOWN,
UV_DIRENT_FILE,
UV_DIRENT_DIR,
UV_DIRENT_LINK,
UV_DIRENT_FIFO,
UV_DIRENT_SOCKET,
UV_DIRENT_CHAR,
UV_DIRENT_BLOCK,
},
os: {
errno: {
EISDIR,
},
},
} = internalBinding('constants');
// The access modes can be any of F_OK, R_OK, W_OK or X_OK. Some might not be
// available on specific systems. They can be used in combination as well
// (F_OK | R_OK | W_OK | X_OK).
const kMinimumAccessMode = MathMin(F_OK, W_OK, R_OK, X_OK);
const kMaximumAccessMode = F_OK | W_OK | R_OK | X_OK;
const kDefaultCopyMode = 0;
// The copy modes can be any of COPYFILE_EXCL, COPYFILE_FICLONE or
// COPYFILE_FICLONE_FORCE. They can be used in combination as well
// (COPYFILE_EXCL | COPYFILE_FICLONE | COPYFILE_FICLONE_FORCE).
const kMinimumCopyMode = MathMin(
kDefaultCopyMode,
COPYFILE_EXCL,
COPYFILE_FICLONE,
COPYFILE_FICLONE_FORCE,
);
const kMaximumCopyMode = COPYFILE_EXCL |
COPYFILE_FICLONE |
COPYFILE_FICLONE_FORCE;
// Most platforms don't allow reads or writes >= 2 GiB.
// See https://github.com/libuv/libuv/pull/1501.
const kIoMaxLength = 2 ** 31 - 1;
// Use 64kb in case the file type is not a regular file and thus do not know the
// actual file size. Increasing the value further results in more frequent over
// allocation for small files and consumes CPU time and memory that should be
// used else wise.
// Use up to 512kb per read otherwise to partition reading big files to prevent
// blocking other threads in case the available threads are all in use.
const kReadFileUnknownBufferLength = 64 * 1024;
const kReadFileBufferLength = 512 * 1024;
const kWriteFileMaxChunkSize = 512 * 1024;
const kMaxUserId = 2 ** 32 - 1;
let fs;
function lazyLoadFs() {
return fs ??= require('fs');
}
function assertEncoding(encoding) {
if (encoding && !Buffer.isEncoding(encoding)) {
const reason = 'is invalid encoding';
throw new ERR_INVALID_ARG_VALUE('encoding', encoding, reason);
}
}
class Dirent {
constructor(name, type, path) {
this.name = name;
this.parentPath = path;
this[kType] = type;
}
isDirectory() {
return this[kType] === UV_DIRENT_DIR;
}
isFile() {
return this[kType] === UV_DIRENT_FILE;
}
isBlockDevice() {
return this[kType] === UV_DIRENT_BLOCK;
}
isCharacterDevice() {
return this[kType] === UV_DIRENT_CHAR;
}
isSymbolicLink() {
return this[kType] === UV_DIRENT_LINK;
}
isFIFO() {
return this[kType] === UV_DIRENT_FIFO;
}
isSocket() {
return this[kType] === UV_DIRENT_SOCKET;
}
}
class DirentFromStats extends Dirent {
constructor(name, stats, path) {
super(name, null, path);
this[kStats] = stats;
}
}
for (const name of ReflectOwnKeys(Dirent.prototype)) {
if (name === 'constructor') {
continue;
}
DirentFromStats.prototype[name] = function() {
return this[kStats][name]();
};
}
function copyObject(source) {
const target = {};
for (const key in source)
target[key] = source[key];
return target;
}
const bufferSep = Buffer.from(pathModule.sep);
function join(path, name) {
if ((typeof path === 'string' || isUint8Array(path)) &&
name === undefined) {
return path;
}
if (typeof path === 'string' && isUint8Array(name)) {
const pathBuffer = Buffer.from(pathModule.join(path, pathModule.sep));
return Buffer.concat([pathBuffer, name]);
}
if (typeof path === 'string' && typeof name === 'string') {
return pathModule.join(path, name);
}
if (isUint8Array(path) && isUint8Array(name)) {
return Buffer.concat([path, bufferSep, name]);
}
throw new ERR_INVALID_ARG_TYPE(
'path', ['string', 'Buffer'], path);
}
function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
const len = names.length;
let toFinish = 0;
callback = once(callback);
for (i = 0; i < len; i++) {
const type = types[i];
if (type === UV_DIRENT_UNKNOWN) {
const name = names[i];
const idx = i;
toFinish++;
let filepath;
try {
filepath = join(path, name);
} catch (err) {
callback(err);
return;
}
lazyLoadFs().lstat(filepath, (err, stats) => {
if (err) {
callback(err);
return;
}
names[idx] = new DirentFromStats(name, stats, path);
if (--toFinish === 0) {
callback(null, names);
}
});
} else {
names[i] = new Dirent(names[i], types[i], path);
}
}
if (toFinish === 0) {
callback(null, names);
}
} else {
const len = names.length;
for (i = 0; i < len; i++) {
names[i] = getDirent(path, names[i], types[i]);
}
return names;
}
}
function getDirent(path, name, type, callback) {
if (typeof callback === 'function') {
if (type === UV_DIRENT_UNKNOWN) {
let filepath;
try {
filepath = join(path, name);
} catch (err) {
callback(err);
return;
}
lazyLoadFs().lstat(filepath, (err, stats) => {
if (err) {
callback(err);
return;
}
callback(null, new DirentFromStats(name, stats, path));
});
} else {
callback(null, new Dirent(name, type, path));
}
} else if (type === UV_DIRENT_UNKNOWN) {
const filepath = join(path, name);
const stats = lazyLoadFs().lstatSync(filepath);
return new DirentFromStats(name, stats, path);
} else {
return new Dirent(name, type, path);
}
}
function getOptions(options, defaultOptions = kEmptyObject) {
if (options == null || typeof options === 'function') {
return defaultOptions;
}
if (typeof options === 'string') {
defaultOptions = { ...defaultOptions };
defaultOptions.encoding = options;
options = defaultOptions;
} else if (typeof options !== 'object') {
throw new ERR_INVALID_ARG_TYPE('options', ['string', 'Object'], options);
}
if (options.encoding !== 'buffer')
assertEncoding(options.encoding);
if (options.signal !== undefined) {
validateAbortSignal(options.signal, 'options.signal');
}
return options;
}
/**
* @param {InternalFSBinding.FSSyncContext} ctx
*/
function handleErrorFromBinding(ctx) {
if (ctx.errno !== undefined) { // libuv error numbers
const err = new UVException(ctx);
ErrorCaptureStackTrace(err, handleErrorFromBinding);
throw err;
}
if (ctx.error !== undefined) { // Errors created in C++ land.
// TODO(joyeecheung): currently, ctx.error are encoding errors
// usually caused by memory problems. We need to figure out proper error
// code(s) for this.
ErrorCaptureStackTrace(ctx.error, handleErrorFromBinding);
throw ctx.error;
}
}
function preprocessSymlinkDestination(path, type, linkPath) {
if (!isWindows) {
// No preprocessing is needed on Unix.
return path;
}
path = '' + path;
if (type === 'junction') {
// Junctions paths need to be absolute and \\?\-prefixed.
// A relative target is relative to the link's parent directory.
path = pathModule.resolve(linkPath, '..', path);
return pathModule.toNamespacedPath(path);
}
if (pathModule.isAbsolute(path)) {
// If the path is absolute, use the \\?\-prefix to enable long filenames
return pathModule.toNamespacedPath(path);
}
// Windows symlinks don't tolerate forward slashes.
return RegExpPrototypeSymbolReplace(/\//g, path, '\\');
}
// Constructor for file stats.
function StatsBase(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks) {
this.dev = dev;
this.mode = mode;
this.nlink = nlink;
this.uid = uid;
this.gid = gid;
this.rdev = rdev;
this.blksize = blksize;
this.ino = ino;
this.size = size;
this.blocks = blocks;
}
StatsBase.prototype.isDirectory = function() {
return this._checkModeProperty(S_IFDIR);
};
StatsBase.prototype.isFile = function() {
return this._checkModeProperty(S_IFREG);
};
StatsBase.prototype.isBlockDevice = function() {
return this._checkModeProperty(S_IFBLK);
};
StatsBase.prototype.isCharacterDevice = function() {
return this._checkModeProperty(S_IFCHR);
};
StatsBase.prototype.isSymbolicLink = function() {
return this._checkModeProperty(S_IFLNK);
};
StatsBase.prototype.isFIFO = function() {
return this._checkModeProperty(S_IFIFO);
};
StatsBase.prototype.isSocket = function() {
return this._checkModeProperty(S_IFSOCK);
};
const kNsPerMsBigInt = 10n ** 6n;
const kNsPerSecBigInt = 10n ** 9n;
const kMsPerSec = 10 ** 3;
const kNsPerMs = 10 ** 6;
function msFromTimeSpec(sec, nsec) {
return sec * kMsPerSec + nsec / kNsPerMs;
}
function nsFromTimeSpecBigInt(sec, nsec) {
return sec * kNsPerSecBigInt + nsec;
}
// The Date constructor performs Math.floor() on the absolute value
// of the timestamp: https://tc39.es/ecma262/#sec-timeclip
// Since there may be a precision loss when the timestamp is
// converted to a floating point number, we manually round
// the timestamp here before passing it to Date().
// Refs: https://github.com/nodejs/node/pull/12607
// Refs: https://github.com/nodejs/node/pull/43714
function dateFromMs(ms) {
// Coercing to number, ms can be bigint
return new Date(MathRound(Number(ms)));
}
const lazyDateFields = {
__proto__: null,
atime: {
__proto__: null,
enumerable: true,
configurable: true,
get() {
return this.atime = dateFromMs(this.atimeMs);
},
set(value) {
ObjectDefineProperty(this, 'atime', { __proto__: null, value, writable: true });
},
},
mtime: {
__proto__: null,
enumerable: true,
configurable: true,
get() {
return this.mtime = dateFromMs(this.mtimeMs);
},
set(value) {
ObjectDefineProperty(this, 'mtime', { __proto__: null, value, writable: true });
},
},
ctime: {
__proto__: null,
enumerable: true,
configurable: true,
get() {
return this.ctime = dateFromMs(this.ctimeMs);
},
set(value) {
ObjectDefineProperty(this, 'ctime', { __proto__: null, value, writable: true });
},
},
birthtime: {
__proto__: null,
enumerable: true,
configurable: true,
get() {
return this.birthtime = dateFromMs(this.birthtimeMs);
},
set(value) {
ObjectDefineProperty(this, 'birthtime', { __proto__: null, value, writable: true });
},
},
};
function BigIntStats(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks,
atimeNs, mtimeNs, ctimeNs, birthtimeNs) {
ReflectApply(StatsBase, this, [dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks]);
this.atimeMs = atimeNs / kNsPerMsBigInt;
this.mtimeMs = mtimeNs / kNsPerMsBigInt;
this.ctimeMs = ctimeNs / kNsPerMsBigInt;
this.birthtimeMs = birthtimeNs / kNsPerMsBigInt;
this.atimeNs = atimeNs;
this.mtimeNs = mtimeNs;
this.ctimeNs = ctimeNs;
this.birthtimeNs = birthtimeNs;
}
ObjectSetPrototypeOf(BigIntStats.prototype, StatsBase.prototype);
ObjectSetPrototypeOf(BigIntStats, StatsBase);
ObjectDefineProperties(BigIntStats.prototype, lazyDateFields);
BigIntStats.prototype._checkModeProperty = function(property) {
if (isWindows && (property === S_IFIFO || property === S_IFBLK ||
property === S_IFSOCK)) {
return false; // Some types are not available on Windows
}
return (this.mode & BigInt(S_IFMT)) === BigInt(property);
};
function Stats(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks,
atimeMs, mtimeMs, ctimeMs, birthtimeMs) {
FunctionPrototypeCall(StatsBase, this, dev, mode, nlink, uid, gid, rdev,
blksize, ino, size, blocks);
this.atimeMs = atimeMs;
this.mtimeMs = mtimeMs;
this.ctimeMs = ctimeMs;
this.birthtimeMs = birthtimeMs;
}
ObjectSetPrototypeOf(Stats.prototype, StatsBase.prototype);
ObjectSetPrototypeOf(Stats, StatsBase);
ObjectDefineProperties(Stats.prototype, lazyDateFields);
Stats.prototype._checkModeProperty = function(property) {
if (isWindows && (property === S_IFIFO || property === S_IFBLK ||
property === S_IFSOCK)) {
return false; // Some types are not available on Windows
}
return (this.mode & S_IFMT) === property;
};
/**
* @param {Float64Array | BigInt64Array} stats
* @param {number} offset
* @returns {BigIntStats | Stats}
*/
function getStatsFromBinding(stats, offset = 0) {
if (isBigInt64Array(stats)) {
return new BigIntStats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], stats[7 + offset], stats[8 + offset],
stats[9 + offset],
nsFromTimeSpecBigInt(stats[10 + offset], stats[11 + offset]),
nsFromTimeSpecBigInt(stats[12 + offset], stats[13 + offset]),
nsFromTimeSpecBigInt(stats[14 + offset], stats[15 + offset]),
nsFromTimeSpecBigInt(stats[16 + offset], stats[17 + offset]),
);
}
return new Stats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], stats[7 + offset], stats[8 + offset],
stats[9 + offset],
msFromTimeSpec(stats[10 + offset], stats[11 + offset]),
msFromTimeSpec(stats[12 + offset], stats[13 + offset]),
msFromTimeSpec(stats[14 + offset], stats[15 + offset]),
msFromTimeSpec(stats[16 + offset], stats[17 + offset]),
);
}
class StatFs {
constructor(type, bsize, blocks, bfree, bavail, files, ffree) {
this.type = type;
this.bsize = bsize;
this.blocks = blocks;
this.bfree = bfree;
this.bavail = bavail;
this.files = files;
this.ffree = ffree;
}
}
function getStatFsFromBinding(stats) {
return new StatFs(
stats[0], stats[1], stats[2], stats[3], stats[4], stats[5], stats[6],
);
}
function stringToFlags(flags, name = 'flags') {
if (typeof flags === 'number') {
validateInt32(flags, name);
return flags;
}
if (flags == null) {
return O_RDONLY;
}
switch (flags) {
case 'r' : return O_RDONLY;
case 'rs' : // Fall through.
case 'sr' : return O_RDONLY | O_SYNC;
case 'r+' : return O_RDWR;
case 'rs+' : // Fall through.
case 'sr+' : return O_RDWR | O_SYNC;
case 'w' : return O_TRUNC | O_CREAT | O_WRONLY;
case 'wx' : // Fall through.
case 'xw' : return O_TRUNC | O_CREAT | O_WRONLY | O_EXCL;
case 'w+' : return O_TRUNC | O_CREAT | O_RDWR;
case 'wx+': // Fall through.
case 'xw+': return O_TRUNC | O_CREAT | O_RDWR | O_EXCL;
case 'a' : return O_APPEND | O_CREAT | O_WRONLY;
case 'ax' : // Fall through.
case 'xa' : return O_APPEND | O_CREAT | O_WRONLY | O_EXCL;
case 'as' : // Fall through.
case 'sa' : return O_APPEND | O_CREAT | O_WRONLY | O_SYNC;
case 'a+' : return O_APPEND | O_CREAT | O_RDWR;
case 'ax+': // Fall through.
case 'xa+': return O_APPEND | O_CREAT | O_RDWR | O_EXCL;
case 'as+': // Fall through.
case 'sa+': return O_APPEND | O_CREAT | O_RDWR | O_SYNC;
}
throw new ERR_INVALID_ARG_VALUE('flags', flags);
}
const stringToSymlinkType = hideStackFrames((type) => {
switch (type) {
case undefined:
case null:
case 'file':
return 0;
case 'dir':
return UV_FS_SYMLINK_DIR;
case 'junction':
return UV_FS_SYMLINK_JUNCTION;
}
});
// converts Date or number to a fractional UNIX timestamp
function toUnixTimestamp(time, name = 'time') {
// eslint-disable-next-line eqeqeq
if (typeof time === 'string' && +time == time) {
return +time;
}
if (NumberIsFinite(time)) {
if (time < 0) {
return DateNow() / 1000;
}
return time;
}
if (isDate(time)) {
// Convert to 123.456 UNIX timestamp
return DatePrototypeGetTime(time) / 1000;
}
throw new ERR_INVALID_ARG_TYPE(name, ['Date', 'Time in seconds'], time);
}
const validateOffsetLengthRead = hideStackFrames(
(offset, length, bufferLength) => {
if (offset < 0) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError('offset', '>= 0', offset);
}
if (length < 0) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError('length', '>= 0', length);
}
if (offset + length > bufferLength) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError('length',
`<= ${bufferLength - offset}`, length);
}
},
);
const validateOffsetLengthWrite = hideStackFrames(
(offset, length, byteLength) => {
if (offset > byteLength) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError('offset', `<= ${byteLength}`, offset);
}
if (length > byteLength - offset) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError('length', `<= ${byteLength - offset}`, length);
}
if (length < 0) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError('length', '>= 0', length);
}
validateInt32.withoutStackTrace(length, 'length', 0);
},
);
const validatePath = hideStackFrames((path, propName = 'path') => {
if (typeof path !== 'string' && !isUint8Array(path)) {
throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(propName, ['string', 'Buffer', 'URL'], path);
}
const pathIsString = typeof path === 'string';
const pathIsUint8Array = isUint8Array(path);
// We can only perform meaningful checks on strings and Uint8Arrays.
if ((!pathIsString && !pathIsUint8Array) ||
(pathIsString && !StringPrototypeIncludes(path, '\u0000')) ||
(pathIsUint8Array && !TypedArrayPrototypeIncludes(path, 0))) {
return;
}
throw new ERR_INVALID_ARG_VALUE.HideStackFramesError(
propName,
path,
'must be a string, Uint8Array, or URL without null bytes',
);
});
const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => {
const path = toPathIfFileURL(fileURLOrPath);
validatePath(path, propName);
return path;
});
const getValidatedFd = hideStackFrames((fd, propName = 'fd') => {
if (ObjectIs(fd, -0)) {
return 0;
}
validateInt32(fd, propName, 0);
return fd;
});
const validateBufferArray = hideStackFrames((buffers, propName = 'buffers') => {
if (!ArrayIsArray(buffers))
throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(propName, 'ArrayBufferView[]', buffers);
for (let i = 0; i < buffers.length; i++) {
if (!isArrayBufferView(buffers[i]))
throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(propName, 'ArrayBufferView[]', buffers);
}
return buffers;
});
let nonPortableTemplateWarn = true;
function warnOnNonPortableTemplate(template) {
// Template strings passed to the mkdtemp() family of functions should not
// end with 'X' because they are handled inconsistently across platforms.
if (nonPortableTemplateWarn &&
((typeof template === 'string' && StringPrototypeEndsWith(template, 'X')) ||
(typeof template !== 'string' && TypedArrayPrototypeAt(template, -1) === 0x58))) {
process.emitWarning('mkdtemp() templates ending with X are not portable. ' +
'For details see: https://nodejs.org/api/fs.html');
nonPortableTemplateWarn = false;
}
}
const defaultCpOptions = {
dereference: false,
errorOnExist: false,
filter: undefined,
force: true,
preserveTimestamps: false,
recursive: false,
verbatimSymlinks: false,
};
const defaultRmOptions = {
recursive: false,
force: false,
retryDelay: 100,
maxRetries: 0,
};
const defaultRmdirOptions = {
retryDelay: 100,
maxRetries: 0,
recursive: false,
};
const validateCpOptions = hideStackFrames((options) => {
if (options === undefined)
return { ...defaultCpOptions };
validateObject(options, 'options');
options = { ...defaultCpOptions, ...options };
validateBoolean(options.dereference, 'options.dereference');
validateBoolean(options.errorOnExist, 'options.errorOnExist');
validateBoolean(options.force, 'options.force');
validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps');
validateBoolean(options.recursive, 'options.recursive');
validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks');
options.mode = getValidMode(options.mode, 'copyFile');
if (options.dereference === true && options.verbatimSymlinks === true) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR.HideStackFramesError('dereference', 'verbatimSymlinks');
}
if (options.filter !== undefined) {
validateFunction(options.filter, 'options.filter');
}
return options;
});
const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => {
options = validateRmdirOptions(options, defaultRmOptions);
validateBoolean(options.force, 'options.force');
lazyLoadFs().lstat(path, (err, stats) => {
if (err) {
if (options.force && err.code === 'ENOENT') {
return cb(null, options);
}
return cb(err, options);
}
if (expectDir && !stats.isDirectory()) {
return cb(false);
}
if (stats.isDirectory() && !options.recursive) {
const err = new ERR_FS_EISDIR.HideStackFramesError({
code: 'EISDIR',
message: 'is a directory',
path,
syscall: 'rm',
errno: EISDIR,
});
return cb(err);
}
return cb(null, options);
});
});
const validateRmOptionsSync = hideStackFrames((path, options, expectDir) => {
options = validateRmdirOptions.withoutStackTrace(options, defaultRmOptions);
validateBoolean.withoutStackTrace(options.force, 'options.force');
if (!options.force || expectDir || !options.recursive) {
const isDirectory = lazyLoadFs()
.lstatSync(path, { throwIfNoEntry: !options.force })?.isDirectory();
if (expectDir && !isDirectory) {
return false;
}
if (isDirectory && !options.recursive) {
throw new ERR_FS_EISDIR.HideStackFramesError({
code: 'EISDIR',
message: 'is a directory',
path,
syscall: 'rm',
errno: EISDIR,
});
}
}
return options;
});
let recursiveRmdirWarned;
function emitRecursiveRmdirWarning() {
if (recursiveRmdirWarned === undefined) {
// TODO(joyeecheung): use getOptionValue('--no-deprecation') instead.
recursiveRmdirWarned = process.noDeprecation;
}
if (!recursiveRmdirWarned) {
process.emitWarning(
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DeprecationWarning',
'DEP0147',
);
recursiveRmdirWarned = true;
}
}
const validateRmdirOptions = hideStackFrames(
(options, defaults = defaultRmdirOptions) => {
if (options === undefined)
return defaults;
validateObject.withoutStackTrace(options, 'options');
options = { ...defaults, ...options };
validateBoolean.withoutStackTrace(options.recursive, 'options.recursive');
validateInt32.withoutStackTrace(options.retryDelay, 'options.retryDelay', 0);
validateUint32.withoutStackTrace(options.maxRetries, 'options.maxRetries');
return options;
});
const getValidMode = hideStackFrames((mode, type) => {
let min = kMinimumAccessMode;
let max = kMaximumAccessMode;
let def = F_OK;
if (type === 'copyFile') {
min = kMinimumCopyMode;
max = kMaximumCopyMode;
def = mode || kDefaultCopyMode;
} else {
assert(type === 'access');
}
if (mode == null) {
return def;
}
validateInteger.withoutStackTrace(mode, 'mode', min, max);
return mode;
});
const validateStringAfterArrayBufferView = hideStackFrames((buffer, name) => {
if (typeof buffer !== 'string') {
throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(
name,
['string', 'Buffer', 'TypedArray', 'DataView'],
buffer,
);
}
});
const validatePosition = hideStackFrames((position, name, length) => {
if (typeof position === 'number') {
validateInteger.withoutStackTrace(position, name, -1);
} else if (typeof position === 'bigint') {
const maxPosition = 2n ** 63n - 1n - BigInt(length);
if (!(position >= -1n && position <= maxPosition)) {
throw new ERR_OUT_OF_RANGE.HideStackFramesError(name,
`>= -1 && <= ${maxPosition}`,
position);
}
} else {
throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(name, ['integer', 'bigint'], position);
}
});
module.exports = {
constants: {
kIoMaxLength,
kMaxUserId,
kReadFileBufferLength,
kReadFileUnknownBufferLength,
kWriteFileMaxChunkSize,
},
assertEncoding,
BigIntStats, // for testing
copyObject,
Dirent,
DirentFromStats,
emitRecursiveRmdirWarning,
getDirent,
getDirents,
getOptions,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
getStatFsFromBinding,
getStatsFromBinding,
stringToFlags,
stringToSymlinkType,
Stats: deprecate(Stats, 'fs.Stats constructor is deprecated.', 'DEP0180'),
toUnixTimestamp,
validateBufferArray,
validateCpOptions,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
validatePosition,
validateRmOptions,
validateRmOptionsSync,
validateRmdirOptions,
validateStringAfterArrayBufferView,
warnOnNonPortableTemplate,
};

387
lib/internal/fs/watchers.js Normal file
View File

@ -0,0 +1,387 @@
'use strict';
const {
FunctionPrototypeCall,
ObjectDefineProperty,
ObjectSetPrototypeOf,
PromiseWithResolvers,
Symbol,
} = primordials;
const {
AbortError,
UVException,
codes: {
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const {
kEmptyObject,
} = require('internal/util');
const {
kFsStatsFieldsNumber,
StatWatcher: _StatWatcher,
} = internalBinding('fs');
const { FSEvent } = internalBinding('fs_event_wrap');
const { UV_ENOSPC } = internalBinding('uv');
const { EventEmitter } = require('events');
const {
getStatsFromBinding,
getValidatedPath,
} = require('internal/fs/utils');
const {
defaultTriggerAsyncIdScope,
symbols: { owner_symbol },
} = require('internal/async_hooks');
const { toNamespacedPath } = require('path');
const {
validateAbortSignal,
validateBoolean,
validateObject,
validateUint32,
} = require('internal/validators');
const {
Buffer: {
isEncoding,
},
} = require('buffer');
const assert = require('internal/assert');
const kOldStatus = Symbol('kOldStatus');
const kUseBigint = Symbol('kUseBigint');
const kFSWatchStart = Symbol('kFSWatchStart');
const kFSStatWatcherStart = Symbol('kFSStatWatcherStart');
const KFSStatWatcherRefCount = Symbol('KFSStatWatcherRefCount');
const KFSStatWatcherMaxRefCount = Symbol('KFSStatWatcherMaxRefCount');
const kFSStatWatcherAddOrCleanRef = Symbol('kFSStatWatcherAddOrCleanRef');
function emitStop(self) {
self.emit('stop');
}
function StatWatcher(bigint) {
FunctionPrototypeCall(EventEmitter, this);
this._handle = null;
this[kOldStatus] = -1;
this[kUseBigint] = bigint;
this[KFSStatWatcherRefCount] = 1;
this[KFSStatWatcherMaxRefCount] = 1;
}
ObjectSetPrototypeOf(StatWatcher.prototype, EventEmitter.prototype);
ObjectSetPrototypeOf(StatWatcher, EventEmitter);
function onchange(newStatus, stats) {
const self = this[owner_symbol];
if (self[kOldStatus] === -1 &&
newStatus === -1 &&
stats[2/* new nlink */] === stats[16/* old nlink */]) {
return;
}
self[kOldStatus] = newStatus;
self.emit('change', getStatsFromBinding(stats),
getStatsFromBinding(stats, kFsStatsFieldsNumber));
}
// At the moment if filename is undefined, we
// 1. Throw an Error if it's the first
// time Symbol('kFSStatWatcherStart') is called
// 2. Return silently if Symbol('kFSStatWatcherStart') has already been called
// on a valid filename and the wrap has been initialized
// This method is a noop if the watcher has already been started.
StatWatcher.prototype[kFSStatWatcherStart] = function(filename,
persistent,
interval) {
if (this._handle !== null)
return;
this._handle = new _StatWatcher(this[kUseBigint]);
this._handle[owner_symbol] = this;
this._handle.onchange = onchange;
if (!persistent)
this.unref();
// uv_fs_poll is a little more powerful than ev_stat but we curb it for
// the sake of backwards compatibility.
this[kOldStatus] = -1;
filename = getValidatedPath(filename, 'filename');
validateUint32(interval, 'interval');
const err = this._handle.start(toNamespacedPath(filename), interval);
if (err) {
const error = new UVException({
errno: err,
syscall: 'watch',
path: filename,
});
error.filename = filename;
throw error;
}
};
// To maximize backward-compatibility for the end user,
// a no-op stub method has been added instead of
// totally removing StatWatcher.prototype.start.
// This should not be documented.
StatWatcher.prototype.start = () => {};
// FIXME(joyeecheung): this method is not documented while there is
// another documented fs.unwatchFile(). The counterpart in
// FSWatcher is .close()
// This method is a noop if the watcher has not been started.
StatWatcher.prototype.stop = function() {
if (this._handle === null)
return;
defaultTriggerAsyncIdScope(this._handle.getAsyncId(),
process.nextTick,
emitStop,
this);
this._handle.close();
this._handle = null;
};
// Clean up or add ref counters.
StatWatcher.prototype[kFSStatWatcherAddOrCleanRef] = function(operate) {
if (operate === 'add') {
// Add a Ref
this[KFSStatWatcherRefCount]++;
this[KFSStatWatcherMaxRefCount]++;
} else if (operate === 'clean') {
// Clean up a single
this[KFSStatWatcherMaxRefCount]--;
this.unref();
} else if (operate === 'cleanAll') {
// Clean up all
this[KFSStatWatcherMaxRefCount] = 0;
this[KFSStatWatcherRefCount] = 0;
this._handle?.unref();
}
};
StatWatcher.prototype.ref = function() {
// Avoid refCount calling ref multiple times causing unref to have no effect.
if (this[KFSStatWatcherRefCount] === this[KFSStatWatcherMaxRefCount])
return this;
if (this._handle && this[KFSStatWatcherRefCount]++ === 0)
this._handle.ref();
return this;
};
StatWatcher.prototype.unref = function() {
// Avoid refCount calling unref multiple times causing ref to have no effect.
if (this[KFSStatWatcherRefCount] === 0) return this;
if (this._handle && --this[KFSStatWatcherRefCount] === 0)
this._handle.unref();
return this;
};
function FSWatcher() {
FunctionPrototypeCall(EventEmitter, this);
this._handle = new FSEvent();
this._handle[owner_symbol] = this;
this._handle.onchange = (status, eventType, filename) => {
// TODO(joyeecheung): we may check self._handle.initialized here
// and return if that is false. This allows us to avoid firing the event
// after the handle is closed, and to fire both UV_RENAME and UV_CHANGE
// if they are set by libuv at the same time.
if (status < 0) {
if (this._handle !== null) {
// We don't use this.close() here to avoid firing the close event.
this._handle.close();
this._handle = null; // Make the handle garbage collectable.
}
const error = new UVException({
errno: status,
syscall: 'watch',
path: filename,
});
error.filename = filename;
this.emit('error', error);
} else {
this.emit('change', eventType, filename);
}
};
}
ObjectSetPrototypeOf(FSWatcher.prototype, EventEmitter.prototype);
ObjectSetPrototypeOf(FSWatcher, EventEmitter);
// At the moment if filename is undefined, we
// 1. Throw an Error if it's the first time Symbol('kFSWatchStart') is called
// 2. Return silently if Symbol('kFSWatchStart') has already been called
// on a valid filename and the wrap has been initialized
// 3. Return silently if the watcher has already been closed
// This method is a noop if the watcher has already been started.
FSWatcher.prototype[kFSWatchStart] = function(filename,
persistent,
recursive,
encoding) {
if (this._handle === null) { // closed
return;
}
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (this._handle.initialized) { // already started
return;
}
filename = getValidatedPath(filename, 'filename');
const err = this._handle.start(toNamespacedPath(filename),
persistent,
recursive,
encoding);
if (err) {
const error = new UVException({
errno: err,
syscall: 'watch',
path: filename,
message: err === UV_ENOSPC ?
'System limit for number of file watchers reached' : '',
});
error.filename = filename;
throw error;
}
};
// To maximize backward-compatibility for the end user,
// a no-op stub method has been added instead of
// totally removing FSWatcher.prototype.start.
// This should not be documented.
FSWatcher.prototype.start = () => {};
// This method is a noop if the watcher has not been started or
// has already been closed.
FSWatcher.prototype.close = function() {
if (this._handle === null) { // closed
return;
}
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (!this._handle.initialized) { // not started
return;
}
this._handle.close();
this._handle = null; // Make the handle garbage collectable.
process.nextTick(emitCloseNT, this);
};
FSWatcher.prototype.ref = function() {
if (this._handle) this._handle.ref();
return this;
};
FSWatcher.prototype.unref = function() {
if (this._handle) this._handle.unref();
return this;
};
function emitCloseNT(self) {
self.emit('close');
}
// Legacy alias on the C++ wrapper object. This is not public API, so we may
// want to runtime-deprecate it at some point. There's no hurry, though.
ObjectDefineProperty(FSEvent.prototype, 'owner', {
__proto__: null,
get() { return this[owner_symbol]; },
set(v) { return this[owner_symbol] = v; },
});
let kResistStopPropagation;
async function* watch(filename, options = kEmptyObject) {
const path = toNamespacedPath(getValidatedPath(filename));
validateObject(options, 'options');
const {
persistent = true,
recursive = false,
encoding = 'utf8',
signal,
} = options;
validateBoolean(persistent, 'options.persistent');
validateBoolean(recursive, 'options.recursive');
validateAbortSignal(signal, 'options.signal');
if (encoding && !isEncoding(encoding)) {
const reason = 'is invalid encoding';
throw new ERR_INVALID_ARG_VALUE('encoding', encoding, reason);
}
if (signal?.aborted)
throw new AbortError(undefined, { cause: signal.reason });
const handle = new FSEvent();
let { promise, resolve, reject } = PromiseWithResolvers();
const oncancel = () => {
handle.close();
reject(new AbortError(undefined, { cause: signal?.reason }));
};
try {
if (signal) {
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
signal.addEventListener('abort', oncancel, { __proto__: null, once: true, [kResistStopPropagation]: true });
}
handle.onchange = (status, eventType, filename) => {
if (status < 0) {
const error = new UVException({
errno: status,
syscall: 'watch',
path: filename,
});
error.filename = filename;
handle.close();
reject(error);
return;
}
resolve({ eventType, filename });
};
const err = handle.start(path, persistent, recursive, encoding);
if (err) {
const error = new UVException({
errno: err,
syscall: 'watch',
path: filename,
message: err === UV_ENOSPC ?
'System limit for number of file watchers reached' : '',
});
error.filename = filename;
handle.close();
throw error;
}
while (!signal?.aborted) {
yield await promise;
({ promise, resolve, reject } = PromiseWithResolvers());
}
throw new AbortError(undefined, { cause: signal?.reason });
} finally {
handle.close();
signal?.removeEventListener('abort', oncancel);
}
}
module.exports = {
FSWatcher,
StatWatcher,
kFSWatchStart,
kFSStatWatcherStart,
kFSStatWatcherAddOrCleanRef,
watch,
};