Implement test-fs-cp.mjs

This commit is contained in:
Marko Vejnovic
2025-09-09 16:29:28 -07:00
parent d6c1b54289
commit ab708e0099
8 changed files with 1339 additions and 50 deletions

View File

@@ -75,10 +75,12 @@ const errors: ErrorCodeMapping = [
["ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE", Error],
["ERR_FORMDATA_PARSE_ERROR", TypeError],
["ERR_FS_CP_DIR_TO_NON_DIR", Error],
["ERR_FS_CP_EEXIST", Error],
["ERR_FS_CP_EINVAL", Error],
["ERR_FS_CP_FIFO_PIPE", Error],
["ERR_FS_CP_NON_DIR_TO_DIR", Error],
["ERR_FS_CP_SOCKET", Error],
["ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY", Error],
["ERR_FS_CP_UNKNOWN", Error],
["ERR_FS_EISDIR", Error],
["ERR_HTTP_BODY_NOT_ALLOWED", Error],

View File

@@ -557,14 +557,21 @@ static JSValue getModuleExtensionsObject(VM& vm, JSObject* moduleObject)
static JSValue getModuleDebugObject(VM& vm, JSObject* moduleObject)
{
return JSC::constructEmptyObject(moduleObject->globalObject());
auto* globalObject = moduleObject->globalObject();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue result = JSC::constructEmptyObject(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return result;
}
static JSValue getPathCacheObject(VM& vm, JSObject* moduleObject)
{
auto* globalObject = defaultGlobalObject(moduleObject->globalObject());
return JSC::constructEmptyObject(
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue result = JSC::constructEmptyObject(
vm, globalObject->nullPrototypeObjectStructure());
RETURN_IF_EXCEPTION(scope, {});
return result;
}
static JSValue getSourceMapFunction(VM& vm, JSObject* moduleObject)
@@ -578,6 +585,9 @@ static JSValue getSourceMapFunction(VM& vm, JSObject* moduleObject)
static JSValue getBuiltinModulesObject(VM& vm, JSObject* moduleObject)
{
auto* globalObject = defaultGlobalObject(moduleObject->globalObject());
auto scope = DECLARE_THROW_SCOPE(vm);
MarkedArgumentBuffer args;
args.ensureCapacity(countof(builtinModuleNames));
@@ -585,15 +595,20 @@ static JSValue getBuiltinModulesObject(VM& vm, JSObject* moduleObject)
args.append(JSC::jsOwnedString(vm, String(builtinModuleNames[i])));
}
auto* globalObject = defaultGlobalObject(moduleObject->globalObject());
return JSC::constructArray(globalObject, static_cast<JSC::ArrayAllocationProfile*>(nullptr), JSC::ArgList(args));
JSValue result = JSC::constructArray(globalObject, static_cast<JSC::ArrayAllocationProfile*>(nullptr), JSC::ArgList(args));
RETURN_IF_EXCEPTION(scope, {});
return result;
}
static JSValue getConstantsObject(VM& vm, JSObject* moduleObject)
{
auto* globalObject = defaultGlobalObject(moduleObject->globalObject());
auto scope = DECLARE_THROW_SCOPE(vm);
auto* compileCacheStatus = JSC::constructEmptyObject(
vm, globalObject->nullPrototypeObjectStructure());
RETURN_IF_EXCEPTION(scope, {});
compileCacheStatus->putDirect(vm, JSC::Identifier::fromString(vm, "FAILED"_s),
JSC::jsNumber(0));
compileCacheStatus->putDirect(
@@ -606,6 +621,8 @@ static JSValue getConstantsObject(VM& vm, JSObject* moduleObject)
auto* constantsObject = JSC::constructEmptyObject(
vm, globalObject->nullPrototypeObjectStructure());
RETURN_IF_EXCEPTION(scope, {});
constantsObject->putDirect(
vm, JSC::Identifier::fromString(vm, "compileCacheStatus"_s),
compileCacheStatus);
@@ -614,9 +631,13 @@ static JSValue getConstantsObject(VM& vm, JSObject* moduleObject)
static JSValue getGlobalPathsObject(VM& vm, JSObject* moduleObject)
{
return JSC::constructEmptyArray(
moduleObject->globalObject(),
auto* globalObject = moduleObject->globalObject();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue result = JSC::constructEmptyArray(
globalObject,
static_cast<ArrayAllocationProfile*>(nullptr), 0);
RETURN_IF_EXCEPTION(scope, {});
return result;
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionSetCJSWrapperItem, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
@@ -690,7 +711,10 @@ JSC_DEFINE_CUSTOM_SETTER(setNodeModuleWrapper,
static JSValue getModulePrototypeObject(VM& vm, JSObject* moduleObject)
{
auto* globalObject = defaultGlobalObject(moduleObject->globalObject());
auto scope = DECLARE_THROW_SCOPE(vm);
auto prototype = constructEmptyObject(globalObject, globalObject->objectPrototype(), 2);
RETURN_IF_EXCEPTION(scope, {});
prototype->putDirectCustomAccessor(
vm, WebCore::clientData(vm)->builtinNames().requirePublicName(),

View File

@@ -3076,7 +3076,10 @@ pub const Arguments = struct {
if (arguments.next()) |arg| {
arguments.eat();
force = arg.toBoolean();
// Keep default true if undefined is passed
if (!arg.isUndefinedOrNull()) {
force = arg.toBoolean();
}
}
if (arguments.next()) |arg| {

View File

@@ -10,8 +10,15 @@ function areIdentical(srcStat, destStat) {
return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev;
}
const normalizePathToArray = path =>
ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(resolve(path), sep), Boolean);
const normalizePathToArray = path => {
// Handle URL objects and strings
const pathStr = typeof path === 'object' && path !== null && path.pathname
? path.pathname
: typeof path === 'object' && path !== null && path.href
? new URL(path.href).pathname
: path;
return ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(resolve(pathStr), sep), Boolean);
};
function isSrcSubdir(src, dest) {
const srcArr = normalizePathToArray(src);
@@ -41,6 +48,7 @@ const {
// opendirSync,
readdirSync,
readlinkSync,
realpathSync,
statSync,
symlinkSync,
unlinkSync,
@@ -48,24 +56,89 @@ const {
} = require("node:fs");
const { dirname, isAbsolute, join, parse, resolve, sep } = require("node:path");
function validateDestinationPath(src, dest) {
// Convert URLs to paths if necessary
// Handle both URL objects and strings
const srcPath = typeof src === 'object' && src !== null ? (src.pathname || src.toString()) : src;
const destPath = typeof dest === 'object' && dest !== null ? (dest.pathname || dest.toString()) : dest;
// Skip validation if paths are not strings (shouldn't happen, but be safe)
if (typeof srcPath !== 'string' || typeof destPath !== 'string') {
return;
}
// Only validate if source exists and is a directory
// (sockets, pipes, etc. are handled elsewhere)
try {
const srcStat = statSync(srcPath);
if (!srcStat.isDirectory()) {
return; // Skip validation for non-directories
}
} catch {
return; // Source doesn't exist, skip validation
}
// Check each parent directory in the destination path to see if any
// are symlinks that point back to the source or its parents
let currentPath = dirname(destPath); // Start with parent of dest
const resolvedSrc = realpathSync(srcPath);
while (currentPath && currentPath !== parse(currentPath).root) {
try {
// Check if this path component exists and might be a symlink
if (existsSync(currentPath)) {
const resolvedPath = realpathSync(currentPath);
// Get the part of dest that comes after this path
const remainingPath = destPath.slice(currentPath.length);
const fullResolvedDest = resolvedPath + remainingPath;
// Check if the resolved destination would be inside the source
if (fullResolvedDest.startsWith(resolvedSrc + sep) || fullResolvedDest === resolvedSrc) {
throw $ERR_FS_CP_EINVAL(`cannot copy ${srcPath} to a subdirectory of self ${destPath}`);
}
}
} catch (err) {
// Re-throw ERR_FS_CP_EINVAL errors
if (err.code === 'ERR_FS_CP_EINVAL') {
throw err;
}
// Ignore other errors (like ENOENT) and continue checking parent directories
}
currentPath = dirname(currentPath);
}
}
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");
// }
const { srcStat, destStat, skipped } = checkPathsSync(src, dest, opts);
// Convert URL objects to paths if necessary
// Use decodeURIComponent to handle URL-encoded characters like %25 -> %
const srcPath = typeof src === 'object' && src !== null
? decodeURIComponent(src.pathname || (src.href ? new URL(src.href).pathname : src.toString()))
: src;
const destPath = typeof dest === 'object' && dest !== null
? decodeURIComponent(dest.pathname || (dest.href ? new URL(dest.href).pathname : dest.toString()))
: dest;
// Check if dest path contains symlinks that would create circular reference
validateDestinationPath(srcPath, destPath);
const { srcStat, destStat, skipped } = checkPathsSync(srcPath, destPath, opts);
if (skipped) return;
checkParentPathsSync(src, srcStat, dest);
return checkParentDir(destStat, src, dest, opts);
checkParentPathsSync(srcPath, srcStat, destPath);
return checkParentDir(destStat, srcPath, destPath, opts);
}
function checkPathsSync(src, dest, opts) {
if (opts.filter) {
const shouldCopy = opts.filter(src, dest);
if ($isPromise(shouldCopy)) {
// throw new ERR_INVALID_RETURN_VALUE("boolean", "filter", shouldCopy);
throw new Error("Expected a boolean from the filter function, but got a promise. Use `fs.promises.cp` instead.");
throw $ERR_INVALID_RETURN_VALUE("boolean", "filter", shouldCopy);
}
if (!shouldCopy) return { __proto__: null, skipped: true };
}
@@ -80,7 +153,7 @@ function checkPathsSync(src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error("src and dest cannot be the same");
throw $ERR_FS_CP_EINVAL("src and dest cannot be the same");
}
if (srcStat.isDirectory() && !destStat.isDirectory()) {
// throw new ERR_FS_CP_DIR_TO_NON_DIR({
@@ -90,7 +163,7 @@ function checkPathsSync(src, dest, opts) {
// errno: EISDIR,
// code: "EISDIR",
// });
throw new Error(`cannot overwrite directory ${src} with non-directory ${dest}`);
throw $ERR_FS_CP_DIR_TO_NON_DIR(`cannot overwrite directory ${src} with non-directory ${dest}`);
}
if (!srcStat.isDirectory() && destStat.isDirectory()) {
// throw new ERR_FS_CP_NON_DIR_TO_DIR({
@@ -100,7 +173,7 @@ function checkPathsSync(src, dest, opts) {
// errno: ENOTDIR,
// code: "ENOTDIR",
// });
throw new Error(`cannot overwrite non-directory ${src} with directory ${dest}`);
throw $ERR_FS_CP_NON_DIR_TO_DIR(`cannot overwrite non-directory ${src} with directory ${dest}`);
}
}
@@ -112,7 +185,7 @@ function checkPathsSync(src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`);
throw $ERR_FS_CP_EINVAL(`cannot copy ${src} to a subdirectory of self ${dest}`);
}
return { __proto__: null, srcStat, destStat, skipped: false };
}
@@ -151,7 +224,7 @@ function checkParentPathsSync(src, srcStat, dest) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`);
throw $ERR_FS_CP_EINVAL(`cannot copy ${src} to a subdirectory of self ${dest}`);
}
return checkParentPathsSync(src, srcStat, destParent);
}
@@ -176,7 +249,7 @@ function getStats(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EISDIR",
// });
throw new Error(`${src} is a directory (not copied)`);
throw $ERR_FS_EISDIR(`${src} is a directory (not copied)`);
} else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
@@ -189,7 +262,7 @@ function getStats(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy a socket file: ${dest}`);
throw $ERR_FS_CP_SOCKET(`cannot copy a socket file: ${dest}`);
} else if (srcStat.isFIFO()) {
// throw new ERR_FS_CP_FIFO_PIPE({
// message: `cannot copy a FIFO pipe: ${dest}`,
@@ -198,7 +271,7 @@ function getStats(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy a FIFO pipe: ${dest}`);
throw $ERR_FS_CP_FIFO_PIPE(`cannot copy a FIFO pipe: ${dest}`);
}
// throw new ERR_FS_CP_UNKNOWN({
// message: `cannot copy an unknown file type: ${dest}`,
@@ -207,7 +280,7 @@ function getStats(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy an unknown file type: ${dest}`);
throw $ERR_FS_CP_UNKNOWN(`cannot copy an unknown file type: ${dest}`);
}
function onFile(srcStat, destStat, src, dest, opts) {
@@ -227,7 +300,7 @@ function mayCopyFile(srcStat, src, dest, opts) {
// errno: EEXIST,
// code: "EEXIST",
// });
throw new Error(`${dest} already exists`);
throw $ERR_FS_CP_EEXIST(`${dest} already exists`);
}
}
@@ -330,7 +403,7 @@ function onLink(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`);
throw $ERR_FS_CP_EINVAL(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`);
}
// Prevent copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
@@ -343,7 +416,7 @@ function onLink(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`);
throw $ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`);
}
return copyLink(resolvedSrc, dest);
}

View File

@@ -14,7 +14,7 @@
// },
// } = 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 { access, chmod, copyFile, lstat, mkdir, opendir, readlink, realpath, stat, symlink, unlink, utimes } = require("node:fs/promises");
const { dirname, isAbsolute, join, parse, resolve, sep } = require("node:path");
const PromisePrototypeThen = $Promise.prototype.$then;
@@ -23,12 +23,78 @@ const ArrayPrototypeFilter = Array.prototype.filter;
const StringPrototypeSplit = String.prototype.split;
const ArrayPrototypeEvery = Array.prototype.every;
async function validateDestinationPath(src, dest) {
// Convert URLs to paths if necessary
// Handle both URL objects and strings
const srcPath = typeof src === 'object' && src !== null ? (src.pathname || src.toString()) : src;
const destPath = typeof dest === 'object' && dest !== null ? (dest.pathname || dest.toString()) : dest;
// Skip validation if paths are not strings (shouldn't happen, but be safe)
if (typeof srcPath !== 'string' || typeof destPath !== 'string') {
return;
}
// Only validate if source exists and is a directory
// (sockets, pipes, etc. are handled elsewhere)
try {
const srcStat = await stat(srcPath);
if (!srcStat.isDirectory()) {
return; // Skip validation for non-directories
}
} catch {
return; // Source doesn't exist, skip validation
}
// Check each parent directory in the destination path to see if any
// are symlinks that point back to the source or its parents
let currentPath = dirname(destPath); // Start with parent of dest
const resolvedSrc = await realpath(srcPath);
while (currentPath && currentPath !== parse(currentPath).root) {
try {
// Check if this path component exists and might be a symlink
const exists = await access(currentPath).then(() => true).catch(() => false);
if (exists) {
const resolvedPath = await realpath(currentPath);
// Get the part of dest that comes after this path
const remainingPath = destPath.slice(currentPath.length);
const fullResolvedDest = resolvedPath + remainingPath;
// Check if the resolved destination would be inside the source
if (fullResolvedDest.startsWith(resolvedSrc + sep) || fullResolvedDest === resolvedSrc) {
throw $ERR_FS_CP_EINVAL(`cannot copy ${srcPath} to a subdirectory of self ${destPath}`);
}
}
} catch (err) {
// Re-throw ERR_FS_CP_EINVAL errors
if (err.code === 'ERR_FS_CP_EINVAL') {
throw err;
}
// Ignore other errors (like ENOENT) and continue checking parent directories
}
currentPath = dirname(currentPath);
}
}
async function cpFn(src, dest, opts) {
const stats = await checkPaths(src, dest, opts);
// Convert URL objects to paths if necessary
// Use decodeURIComponent to handle URL-encoded characters like %25 -> %
const srcPath = typeof src === 'object' && src !== null
? decodeURIComponent(src.pathname || (src.href ? new URL(src.href).pathname : src.toString()))
: src;
const destPath = typeof dest === 'object' && dest !== null
? decodeURIComponent(dest.pathname || (dest.href ? new URL(dest.href).pathname : dest.toString()))
: dest;
// Check if dest path contains symlinks that would create circular reference
await validateDestinationPath(srcPath, destPath);
const stats = await checkPaths(srcPath, destPath, opts);
const { srcStat, destStat, skipped } = stats;
if (skipped) return;
await checkParentPaths(src, srcStat, dest);
return checkParentDir(destStat, src, dest, opts);
await checkParentPaths(srcPath, srcStat, destPath);
return checkParentDir(destStat, srcPath, destPath, opts);
}
async function checkPaths(src, dest, opts) {
@@ -38,7 +104,7 @@ 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 $ERR_FS_CP_EINVAL("Source and destination must not be the same.");
}
if (srcStat.isDirectory() && !destStat.isDirectory()) {
// throw new ERR_FS_CP_DIR_TO_NON_DIR({
@@ -48,7 +114,7 @@ async function checkPaths(src, dest, opts) {
// errno: EISDIR,
// code: "EISDIR",
// });
throw new Error(`cannot overwrite directory ${src} with non-directory ${dest}`);
throw $ERR_FS_CP_DIR_TO_NON_DIR(`cannot overwrite directory ${src} with non-directory ${dest}`);
}
if (!srcStat.isDirectory() && destStat.isDirectory()) {
// throw new ERR_FS_CP_NON_DIR_TO_DIR({
@@ -58,7 +124,7 @@ async function checkPaths(src, dest, opts) {
// errno: ENOTDIR,
// code: "ENOTDIR",
// });
throw new Error(`cannot overwrite non-directory ${src} with directory ${dest}`);
throw $ERR_FS_CP_NON_DIR_TO_DIR(`cannot overwrite non-directory ${src} with directory ${dest}`);
}
}
@@ -70,7 +136,7 @@ async function checkPaths(src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`);
throw $ERR_FS_CP_EINVAL(`cannot copy ${src} to a subdirectory of self ${dest}`);
}
return { __proto__: null, srcStat, destStat, skipped: false };
}
@@ -131,13 +197,20 @@ async function checkParentPaths(src, srcStat, dest) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy ${src} to a subdirectory of self ${dest}`);
throw $ERR_FS_CP_EINVAL(`cannot copy ${src} to a subdirectory of self ${dest}`);
}
return checkParentPaths(src, srcStat, destParent);
}
const normalizePathToArray = path =>
ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(resolve(path), sep), Boolean);
const normalizePathToArray = path => {
// Handle URL objects and strings
const pathStr = typeof path === 'object' && path !== null && path.pathname
? path.pathname
: typeof path === 'object' && path !== null && path.href
? new URL(path.href).pathname
: path;
return ArrayPrototypeFilter.$call(StringPrototypeSplit.$call(resolve(pathStr), sep), Boolean);
};
// Return true if dest is a subdir of src, otherwise false.
// It only checks the path strings.
@@ -160,7 +233,7 @@ async function getStatsForCopy(destStat, src, dest, opts) {
// errno: EISDIR,
// code: "EISDIR",
// });
throw new Error(`${src} is a directory (not copied)`);
throw $ERR_FS_EISDIR(`${src} is a directory (not copied)`);
} else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
@@ -173,7 +246,7 @@ async function getStatsForCopy(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy a socket file: ${dest}`);
throw $ERR_FS_CP_SOCKET(`cannot copy a socket file: ${dest}`);
} else if (srcStat.isFIFO()) {
// throw new ERR_FS_CP_FIFO_PIPE({
// message: `cannot copy a FIFO pipe: ${dest}`,
@@ -182,7 +255,7 @@ async function getStatsForCopy(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy a FIFO pipe: ${dest}`);
throw $ERR_FS_CP_FIFO_PIPE(`cannot copy a FIFO pipe: ${dest}`);
}
// throw new ERR_FS_CP_UNKNOWN({
// message: `cannot copy an unknown file type: ${dest}`,
@@ -191,7 +264,7 @@ async function getStatsForCopy(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy an unknown file type: ${dest}`);
throw $ERR_FS_CP_UNKNOWN(`cannot copy an unknown file type: ${dest}`);
}
function onFile(srcStat, destStat, src, dest, opts) {
@@ -211,7 +284,7 @@ async function mayCopyFile(srcStat, src, dest, opts) {
// errno: EEXIST,
// code: "EEXIST",
// });
throw new Error(`${dest} already exists`);
throw $ERR_FS_CP_EEXIST(`${dest} already exists`);
}
}
@@ -312,7 +385,7 @@ async function onLink(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`);
throw $ERR_FS_CP_EINVAL(`cannot copy ${resolvedSrc} to a subdirectory of self ${resolvedDest}`);
}
// Do not copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
@@ -326,7 +399,7 @@ async function onLink(destStat, src, dest, opts) {
// errno: EINVAL,
// code: "EINVAL",
// });
throw new Error(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`);
throw $ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY(`cannot overwrite ${resolvedDest} with ${resolvedSrc}`);
}
return copyLink(resolvedSrc, dest);
}

View File

@@ -103,7 +103,7 @@ function watch(
function cp(src, dest, options) {
if (!options) return fs.cp(src, dest);
if (typeof options !== "object") {
throw new TypeError("options must be an object");
throw $ERR_INVALID_ARG_TYPE("options", "object", options);
}
if (options.dereference || options.filter || options.preserveTimestamps || options.verbatimSymlinks) {
return require("internal/fs/cp")(src, dest, options);

View File

@@ -997,12 +997,50 @@ realpathSync.native = fs.realpathNativeSync.bind(fs);
// and on MacOS, simple cases of recursive directory trees can be done in a single `clonefile()`
// using filter and other options uses a lazily loaded js fallback ported from node.js
function cpSync(src, dest, options) {
if (!options) return fs.cpSync(src, dest);
if (typeof options !== "object") {
throw new TypeError("options must be an object");
if (!options) {
// Check if src and dest are the same for no-options case
if (src === dest) {
throw $ERR_FS_CP_EINVAL("Cannot copy", src, "to itself");
}
// Use JS implementation to ensure proper symlink validation
// The native implementation doesn't validate symlinks in dest path
return require("internal/fs/cp-sync")(src, dest, { force: true });
}
if (options.dereference || options.filter || options.preserveTimestamps || options.verbatimSymlinks) {
return require("internal/fs/cp-sync")(src, dest, options);
if (typeof options !== "object") {
throw $ERR_INVALID_ARG_TYPE("options", "object", options);
}
// Validate verbatimSymlinks is a boolean if provided
if ("verbatimSymlinks" in options && typeof options.verbatimSymlinks !== "boolean") {
throw $ERR_INVALID_ARG_TYPE("options.verbatimSymlinks", "boolean", options.verbatimSymlinks);
}
// Validate mode is a valid integer if provided
if ("mode" in options) {
const mode = options.mode;
if (!Number.isInteger(mode) || mode < 0) {
throw $ERR_OUT_OF_RANGE("options.mode", ">= 0", mode);
}
}
// Check for incompatible options
if (options.dereference && options.verbatimSymlinks) {
throw $ERR_INCOMPATIBLE_OPTION_PAIR("options.dereference", "options.verbatimSymlinks");
}
// Check if src and dest are the same after validations
if (src === dest) {
throw $ERR_FS_CP_EINVAL("Cannot copy", src, "to itself");
}
// Use JS fallback when special options are used
// IMPORTANT: The native implementation doesn't correctly handle symlink resolution
// (it copies symlinks as-is instead of resolving relative paths to absolute).
// So we always use the JS fallback for now to ensure correct behavior.
// Only use native implementation for simple cases without symlinks.
// TODO: Fix the native implementation to handle symlink resolution correctly
if (options.dereference || options.filter || options.preserveTimestamps || "verbatimSymlinks" in options || true) {
// Ensure force defaults to true if not specified
const optsWithDefaults = {
...options,
force: options.force ?? true
};
return require("internal/fs/cp-sync")(src, dest, optsWithDefaults);
}
return fs.cpSync(src, dest, options.recursive, options.errorOnExist, options.force ?? true, options.mode);
}

File diff suppressed because it is too large Load Diff