From 5c9f6d93cd565a82eb0fa1fcaf09b0bfc6d0a0d0 Mon Sep 17 00:00:00 2001 From: yohannesjoya Date: Sun, 8 Nov 2026 11:51:36 +0300 Subject: [PATCH] fs: stat src through symlinks in cpSync() when dereference is set CpSyncCheckPaths() used symlink_status() for src when dereference was true and status() when it was false, the reverse of what the option means. #59681 fixed the same swap for dest but left src as it was. A symlink to a file passed as src with dereference: true was treated as a directory, so cpSync() threw ERR_FS_EISDIR unless recursive was set, while fs.cp() copied the file. Swap the two calls and drop the check that counted a symlink as a directory, which status() can no longer return. Refs: https://github.com/nodejs/node/issues/59168 Refs: https://github.com/nodejs/node/pull/60945 Co-authored-by: Nicholas Paun Signed-off-by: yohannesjoya --- src/node_file.cc | 8 ++-- ...test-fs-cp-dereference-symlink-to-file.mjs | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-fs-cp-dereference-symlink-to-file.mjs diff --git a/src/node_file.cc b/src/node_file.cc index c2cd5998e6fc..e8b06a1c3fd1 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -4616,8 +4616,8 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo& args) { std::error_code error_code; auto src_status = dereference - ? std::filesystem::symlink_status(src_path, error_code) - : std::filesystem::status(src_path, error_code); + ? std::filesystem::status(src_path, error_code) + : std::filesystem::symlink_status(src_path, error_code); if (error_code) { #ifdef _WIN32 @@ -4635,9 +4635,7 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo& args) { bool dest_exists = !error_code && dest_status.type() != std::filesystem::file_type::not_found; - bool src_is_dir = - (src_status.type() == std::filesystem::file_type::directory) || - (dereference && src_status.type() == std::filesystem::file_type::symlink); + bool src_is_dir = src_status.type() == std::filesystem::file_type::directory; auto src_path_str = ConvertPathToUTF8(src_path); auto dest_path_str = ConvertPathToUTF8(dest_path); diff --git a/test/parallel/test-fs-cp-dereference-symlink-to-file.mjs b/test/parallel/test-fs-cp-dereference-symlink-to-file.mjs new file mode 100644 index 000000000000..8c921630366c --- /dev/null +++ b/test/parallel/test-fs-cp-dereference-symlink-to-file.mjs @@ -0,0 +1,42 @@ +// This tests that cp() and cpSync() copy the target file of a symlink passed +// as src when dereference is true and recursive is not set. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { + cp, + cpSync, + lstatSync, + mkdirSync, + readFileSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src); +writeFileSync(join(src, 'foo.txt'), 'foo'); +symlinkSync(join(src, 'foo.txt'), join(src, 'link.txt')); + +const opts = mustNotMutateObjectDeep({ dereference: true }); + +{ + const dest = join(nextdir(), 'foo.txt'); + cpSync(join(src, 'link.txt'), dest, opts); + assert(lstatSync(dest).isFile()); + assert.strictEqual(readFileSync(dest, 'utf8'), 'foo'); +} + +{ + const dest = join(nextdir(), 'foo.txt'); + cp(join(src, 'link.txt'), dest, opts, mustCall((err) => { + assert.ifError(err); + assert(lstatSync(dest).isFile()); + assert.strictEqual(readFileSync(dest, 'utf8'), 'foo'); + })); +}