Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
439 changes: 284 additions & 155 deletions src/js/internal/fs/cp-sync.ts

Large diffs are not rendered by default.

308 changes: 161 additions & 147 deletions src/js/internal/fs/cp.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
// Taken and modified from node.js: https://github.com/nodejs/node/blob/main/lib/internal/fs/cp/cp.js
const {
errno: { EEXIST, EINVAL, EISDIR, ENOTDIR },
fsCpDirToNonDirError,
fsCpEExistError,
fsCpEinvalError,
fsCpFifoPipeError,
fsCpNonDirToDirError,
fsCpSocketError,
fsCpSymlinkToSubdirectoryError,
fsCpUnknownError,
fsEisdirError,
areIdentical,
isSrcSubdir,
} = require("internal/fs/cp-sync");

// 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 { EEXIST, EISDIR, EINVAL, ENOTDIR } = $processBindingConstants.os.errno;
const { chmod, copyFile, lstat, mkdir, opendir, readlink, stat, symlink, unlink, utimes } = require("node:fs/promises");
const { dirname, isAbsolute, join, parse, resolve, sep } = require("node:path");
const { dirname, isAbsolute, join, parse, resolve } = require("node:path");

const PromisePrototypeThen = $Promise.prototype.$then;
const PromiseReject = Promise.$reject;
const ArrayPrototypeFilter = Array.prototype.filter;
const StringPrototypeSplit = String.prototype.split;
const ArrayPrototypeEvery = Array.prototype.every;

async function cpFn(src, dest, opts) {
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))) {
Expand All @@ -38,47 +27,46 @@ async function checkPaths(src, dest, opts) {
const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts);
if (destStat) {
if (areIdentical(srcStat, destStat)) {
throw new Error("Source and destination must not be the same.");
throw fsCpEinvalError({
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 directory ${src} with non-directory ${dest}`,
// path: dest,
// syscall: "cp",
// errno: EISDIR,
// code: "EISDIR",
// });
throw new Error(`cannot overwrite directory ${src} with non-directory ${dest}`);
throw fsCpDirToNonDirError({
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 non-directory ${src} with directory ${dest}`,
// path: dest,
// syscall: "cp",
// errno: ENOTDIR,
// code: "ENOTDIR",
// });
throw new Error(`cannot overwrite non-directory ${src} with directory ${dest}`);
throw fsCpNonDirToDirError({
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",
// });
throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`);
throw fsCpEinvalError({
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 Promise.all([
Expand All @@ -90,22 +78,6 @@ function getStats(src, dest, opts) {
]);
}

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.$call(
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
Expand All @@ -124,27 +96,64 @@ async function checkParentPaths(src, srcStat, dest) {
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",
// });
throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`);
throw fsCpEinvalError({
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.$call(StringPrototypeSplit.$call(resolve(path), sep), Boolean);
// node-correct validation before handing off to the native fast path
// (which performs the copy but does not implement node's cp error codes).
async function tryNativeFastPath(src, dest, opts) {
const checked = await checkPaths(src, dest, opts);
const { srcStat, destStat } = checked;
await checkParentPaths(src, srcStat, dest);
if (srcStat.isDirectory() && !opts.recursive) {
throw fsEisdirError({
message: `${src} is a directory (not copied)`,
path: src,
syscall: "cp",
errno: EISDIR,
code: "EISDIR",
});
}
// The native copy is only node-equivalent for regular-file -> regular-file
// (or missing dest). Symlinks (node resolves relative link targets),
// directories (may contain symlinks), and special files (node-specific
// error codes) must go through the ported implementation.
return { ok: srcStat.isFile() && (!destStat || destStat.isFile()), checked };
}

// 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.$call(srcArr, (cur, i) => destArr[i] === cur);
async function cpFn(src, dest, opts, checked?) {
// `checked` carries the stats from a preceding tryNativeFastPath so the
// fallback doesn't re-run the same checkPaths/checkParentPaths syscalls.
const { srcStat, destStat, skipped } = checked ?? (await checkPaths(src, dest, opts));
if (skipped) return;
if (checked === undefined) await checkParentPaths(src, srcStat, dest);
return checkParentDir(destStat, src, dest, opts);
}

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 pathExistsFulfilled() {
return true;
}
function pathExistsRejected(err) {
return err.code === "ENOENT" ? false : PromiseReject(err);
}
function pathExists(dest) {
return PromisePrototypeThen.$call(stat(dest), pathExistsFulfilled, pathExistsRejected);
}

async function getStatsForCopy(destStat, src, dest, opts) {
Expand All @@ -153,45 +162,41 @@ async function getStatsForCopy(destStat, src, dest, opts) {
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",
// });
throw new Error(`${src} is a directory (not copied)`);
throw fsEisdirError({
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",
// });
throw new Error(`cannot copy a socket file: ${dest}`);
throw fsCpSocketError({
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 Error(`cannot copy a FIFO pipe: ${dest}`);
throw fsCpFifoPipeError({
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",
// });
throw new Error(`cannot copy an unknown file type: ${dest}`);
throw fsCpUnknownError({
message: `cannot copy an unknown file type: ${dest}`,
path: dest,
syscall: "cp",
errno: EINVAL,
code: "EINVAL",
});
}

function onFile(srcStat, destStat, src, dest, opts) {
Expand All @@ -204,14 +209,13 @@ async function mayCopyFile(srcStat, src, dest, opts) {
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",
// });
throw new Error(`${dest} already exists`);
throw fsCpEExistError({
message: `${dest} already exists`,
path: dest,
syscall: "cp",
errno: EEXIST,
code: "EEXIST",
});
}
}

Expand Down Expand Up @@ -261,6 +265,15 @@ async function setDestTimestamps(src, dest) {

function onDir(srcStat, destStat, src, dest, opts) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
if (opts.errorOnExist && !opts.force) {
throw fsCpEExistError({
message: `${dest} already exists`,
path: dest,
syscall: "cp",
errno: EEXIST,
code: "EEXIST",
});
}
return copyDir(src, dest, opts);
}

Expand Down Expand Up @@ -304,29 +317,30 @@ async function onLink(destStat, src, dest, opts) {
if (!isAbsolute(resolvedDest)) {
resolvedDest = resolve(dirname(dest), resolvedDest);
}
if (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",
// });
throw new Error(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`);
// stat(src) follows the link; a dangling src symlink throws ENOENT here,
// same as before (both gated checks below only apply to directories).
const srcStat = await stat(src);
const srcIsDir = srcStat.isDirectory();
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) {
throw fsCpEinvalError({
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",
// });
throw new Error(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`);
if (srcIsDir && isSrcSubdir(resolvedDest, resolvedSrc)) {
throw fsCpSymlinkToSubdirectoryError({
message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
path: dest,
syscall: "cp",
errno: EINVAL,
code: "EINVAL",
});
}
return copyLink(resolvedSrc, dest);
}
Expand All @@ -336,4 +350,4 @@ async function copyLink(resolvedSrc, dest) {
return symlink(resolvedSrc, dest);
}

export default cpFn;
export default { cpFn, tryNativeFastPath };
Loading
Loading