Skip to content
Open
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
103 changes: 85 additions & 18 deletions src/js/internal/fs/cp-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const { EEXIST, EISDIR, EINVAL, ENOTDIR } = $processBindingConstants.os.errno;

const ArrayPrototypeEvery = Array.prototype.every;
const ArrayPrototypeFilter = Array.prototype.filter;
const BufferPrototypeLatin1Slice = Buffer.prototype.latin1Slice;
const StringPrototypeSplit = String.prototype.split;

// COPYFILE_EXCL | COPYFILE_FICLONE | COPYFILE_FICLONE_FORCE
Expand Down Expand Up @@ -146,12 +147,77 @@ function areIdentical(srcStat, destStat) {
return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev;
}

// node:path inspects only ASCII, so a Buffer path is viewed as latin1 (one byte per code unit) for its arithmetic.
const isBufferPath = p => typeof p !== "string";

function latin1View(p) {
// A string meets a Buffer path through its UTF-8 bytes.
return BufferPrototypeLatin1Slice.$call(typeof p === "string" ? Buffer.from(p) : p);
}

function fromLatin1View(view) {
return Buffer.from(view, "latin1");
}

// resolve() would fall back to process.cwd() as UTF-16; pass its view instead.
function cwdView() {
return latin1View(process.cwd());
}

const kBufferEncoding = { encoding: "buffer" };

// readdir/readlink options that return bytes for a Buffer path.
function encodingFor(p) {
return isBufferPath(p) ? kBufferEncoding : undefined;
}

// dirname(p), in the form of p.
function parentPath(p) {
return isBufferPath(p) ? fromLatin1View(dirname(latin1View(p))) : dirname(p);
}

// join(dir, name), as bytes when either is bytes.
function joinDirEntry(dir, name) {
if (!isBufferPath(dir) && !isBufferPath(name)) return join(dir, name);
return fromLatin1View(join(latin1View(dir), latin1View(name)));
}

// The target for the copy of the link at linkPath, from what readlink returned.
function copiedLinkTarget(linkPath, target, verbatim) {
if (!isBufferPath(linkPath) && !isBufferPath(target)) {
return verbatim || isAbsolute(target) ? target : resolve(dirname(linkPath), target);
}
const targetView = latin1View(target);
if (verbatim || isAbsolute(targetView)) return target;
return fromLatin1View(resolve(cwdView(), dirname(latin1View(linkPath)), targetView));
}

// resolve(dirname()) of src and dest for checkParentPaths; destParentPath is the form that goes to stat.
function resolvedParents(src, dest) {
if (!isBufferPath(src) && !isBufferPath(dest)) {
const destParent = resolve(dirname(dest));
return { srcParent: resolve(dirname(src)), destParent, destParentPath: destParent };
}
const cwd = cwdView();
const destParent = resolve(cwd, dirname(latin1View(dest)));
return {
srcParent: resolve(cwd, dirname(latin1View(src))),
destParent,
destParentPath: fromLatin1View(destParent),
};
}

const normalizePathToArray = path =>
ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(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) {
if (isBufferPath(src) || isBufferPath(dest)) {
const cwd = cwdView();
src = resolve(cwd, latin1View(src));
dest = resolve(cwd, latin1View(dest));
}
const srcArr = normalizePathToArray(src);
const destArr = normalizePathToArray(dest);
return ArrayPrototypeEvery.$call(srcArr, (cur, i) => destArr[i] === cur);
Expand Down Expand Up @@ -225,12 +291,11 @@ function getStatsSync(src, dest, opts) {
}

function checkParentPathsSync(src, srcStat, dest) {
const srcParent = resolve(dirname(src));
const destParent = resolve(dirname(dest));
const { srcParent, destParent, destParentPath } = resolvedParents(src, dest);
if (destParent === srcParent || destParent === parse(destParent).root) return;
let destStat;
try {
destStat = statSync(destParent, { bigint: true });
destStat = statSync(destParentPath, { bigint: true });
} catch (err: any) {
if (err.code === "ENOENT") return;
throw err;
Expand All @@ -244,7 +309,7 @@ function checkParentPathsSync(src, srcStat, dest) {
code: "EINVAL",
});
}
return checkParentPathsSync(src, srcStat, destParent);
return checkParentPathsSync(src, srcStat, destParentPath);
}

// The native recursive copy (a single clonefile() on macOS) copies symlinks
Expand All @@ -267,7 +332,7 @@ function treeContainsOnlyFilesAndDirsSync(root) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (entry.isDirectory()) {
stack.push(join(dir, entry.name));
stack.push(joinDirEntry(dir, entry.name));
} else if (!entry.isFile()) {
return false;
}
Expand Down Expand Up @@ -318,7 +383,7 @@ function cpSyncFn(src, dest, opts, checked?) {
}

function checkParentDir(destStat, src, dest, opts) {
const destParent = dirname(dest);
const destParent = parentPath(dest);
if (!existsSync(destParent)) mkdirSync(destParent, { recursive: true });
return getStats(destStat, src, dest, opts);
}
Expand Down Expand Up @@ -442,26 +507,23 @@ function mkDirAndCopy(srcMode, src, dest, opts) {
}

function copyDir(src, dest, opts) {
for (const dirent of readdirSync(src, { withFileTypes: true })) {
const { name } = dirent;
const srcItem = join(src, name);
const destItem = join(dest, name);
// Entry names under a Buffer directory are bytes too.
for (const name of readdirSync(src, encodingFor(src))) {
const srcItem = joinDirEntry(src, name);
const destItem = joinDirEntry(dest, name);
const { destStat, skipped } = checkPathsSync(srcItem, destItem, opts);
if (!skipped) getStats(destStat, srcItem, destItem, opts);
}
}

function onLink(destStat, src, dest, opts) {
let resolvedSrc = readlinkSync(src);
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
resolvedSrc = resolve(dirname(src), resolvedSrc);
}
const resolvedSrc = copiedLinkTarget(src, readlinkSync(src, encodingFor(src)), opts.verbatimSymlinks);
if (!destStat) {
return symlinkSync(resolvedSrc, dest);
}
let resolvedDest;
try {
resolvedDest = readlinkSync(dest);
resolvedDest = readlinkSync(dest, encodingFor(dest));
} catch (err: any) {
// Dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
Expand All @@ -471,9 +533,7 @@ function onLink(destStat, src, dest, opts) {
}
throw err;
}
if (!isAbsolute(resolvedDest)) {
resolvedDest = resolve(dirname(dest), resolvedDest);
}
resolvedDest = copiedLinkTarget(dest, resolvedDest, false);
let srcIsDir = false;
try {
srcIsDir = statSync(src).isDirectory();
Expand Down Expand Up @@ -511,6 +571,13 @@ export default {
cpSyncFn,
validateCpOptions,
tryNativeFastPathSync,
isBufferPath,
kBufferEncoding,
encodingFor,
parentPath,
joinDirEntry,
copiedLinkTarget,
resolvedParents,
errno: { EEXIST, EISDIR, EINVAL, ENOTDIR },
fsCpDirToNonDirError,
fsCpEExistError,
Expand Down
50 changes: 31 additions & 19 deletions src/js/internal/fs/cp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ const {
fsEisdirError,
areIdentical,
isSrcSubdir,
isBufferPath,
kBufferEncoding,
encodingFor,
parentPath,
joinDirEntry,
copiedLinkTarget,
resolvedParents,
} = require("internal/fs/cp-sync");

const {
Expand All @@ -27,7 +34,7 @@ const {
unlink,
utimes,
} = require("node:fs/promises");
const { dirname, isAbsolute, join, parse, resolve } = require("node:path");
const { parse } = require("node:path");

const PromisePrototypeThen = $Promise.prototype.$then;
const PromiseReject = Promise.$reject;
Expand Down Expand Up @@ -95,14 +102,13 @@ function getStats(src, dest, opts) {
// 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));
const { srcParent, destParent, destParentPath } = resolvedParents(src, dest);
if (destParent === srcParent || destParent === parse(destParent).root) {
return;
}
let destStat;
try {
destStat = await stat(destParent, { bigint: true });
destStat = await stat(destParentPath, { bigint: true });
} catch (err: any) {
if (err.code === "ENOENT") return;
throw err;
Expand All @@ -116,7 +122,7 @@ async function checkParentPaths(src, srcStat, dest) {
code: "EINVAL",
});
}
return checkParentPaths(src, srcStat, destParent);
return checkParentPaths(src, srcStat, destParentPath);
}

// The native recursive copy (a single clonefile() on macOS) copies symlinks
Expand All @@ -139,7 +145,7 @@ async function treeContainsOnlyFilesAndDirs(root) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (entry.isDirectory()) {
stack.push(join(dir, entry.name));
stack.push(joinDirEntry(dir, entry.name));
} else if (!entry.isFile()) {
return false;
}
Expand Down Expand Up @@ -190,7 +196,7 @@ async function cpFn(src, dest, opts, checked?) {
}

async function checkParentDir(destStat, src, dest, opts) {
const destParent = dirname(dest);
const destParent = parentPath(dest);
const dirExists = await pathExists(destParent);
if (dirExists) return getStatsForCopy(destStat, src, dest, opts);
await mkdir(destParent, { recursive: true });
Expand Down Expand Up @@ -335,27 +341,35 @@ async function mkDirAndCopy(srcMode, src, dest, opts) {
}

async function copyDir(src, dest, opts) {
if (isBufferPath(src)) {
// opendir() cannot return entry names as bytes (#27914), readdir() can.
for (const name of await readdir(src, kBufferEncoding)) {
await copyDirEntry(src, dest, name, opts);
}
return;
}
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);
await copyDirEntry(src, dest, name, opts);
}
}

async function copyDirEntry(src, dest, name, opts) {
const srcItem = joinDirEntry(src, name);
const destItem = joinDirEntry(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);
}
const resolvedSrc = copiedLinkTarget(src, await readlink(src, encodingFor(src)), opts.verbatimSymlinks);
if (!destStat) {
return symlink(resolvedSrc, dest);
}
let resolvedDest;
try {
resolvedDest = await readlink(dest);
resolvedDest = await readlink(dest, encodingFor(dest));
} catch (err: any) {
// Dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
Expand All @@ -365,9 +379,7 @@ async function onLink(destStat, src, dest, opts) {
}
throw err;
}
if (!isAbsolute(resolvedDest)) {
resolvedDest = resolve(dirname(dest), resolvedDest);
}
resolvedDest = copiedLinkTarget(dest, resolvedDest, false);
// 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);
Expand Down
Loading