mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 20:39:05 +00:00
Implement test-fs-cp.mjs
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
1076
test/js/node/test/parallel/test-fs-cp.mjs
Normal file
1076
test/js/node/test/parallel/test-fs-cp.mjs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user