Skip to content

Commit d659dd8

Browse files
codebytereaduh95
authored andcommitted
fs: give directories created by cpSync the source directory's mode
The C++ fast path that fs.cpSync() takes when no filter is given created the destination directories with default permissions, so a 0700 directory came out of the copy as 0755 (with the default umask). The JavaScript implementation, which fs.cp(), fs.promises.cp() and fs.cpSync() with a filter still use, chmod()s every directory it creates to the mode of its source, and so did cpSync before the port. Set the source directory's permissions on each directory the copy creates (the destination root included); directories that already exist keep theirs, as before. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65488 Refs: #58461 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 4b23a44 commit d659dd8

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

src/node_file.cc

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3791,6 +3791,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
37913791
auto dest_path = dest.ToPath();
37923792

37933793
std::error_code error;
3794+
const bool dest_existed = std::filesystem::exists(dest_path, error);
37943795
std::filesystem::create_directories(dest_path, error);
37953796
if (error) {
37963797
return env->ThrowStdErrException(error, "cp", *dest);
@@ -3910,11 +3911,28 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
39103911
}
39113912
} else if (dir_entry.is_directory()) {
39123913
auto entry_dir_path = src / dir_entry.path().filename();
3913-
std::filesystem::create_directory(dest_file_path);
3914+
const bool created =
3915+
std::filesystem::create_directory(dest_file_path, error);
3916+
if (error) {
3917+
env->ThrowStdErrException(
3918+
error, "cp", ConvertPathToUTF8(dest_file_path).c_str());
3919+
return false;
3920+
}
39143921
auto success = copy_dir_contents(entry_dir_path, dest_file_path);
39153922
if (!success) {
39163923
return false;
39173924
}
3925+
// A directory created by the copy gets the mode of its source once
3926+
// its contents are in (the source may be read-only).
3927+
if (created) {
3928+
std::filesystem::permissions(
3929+
dest_file_path, dir_entry.status().permissions(), error);
3930+
if (error) {
3931+
env->ThrowStdErrException(
3932+
error, "cp", ConvertPathToUTF8(dest_file_path).c_str());
3933+
return false;
3934+
}
3935+
}
39183936
} else if (dir_entry.is_regular_file()) {
39193937
std::filesystem::copy_file(
39203938
dir_entry.path(), dest_file_path, file_copy_opts, error);
@@ -3939,7 +3957,13 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
39393957
return true;
39403958
};
39413959

3942-
copy_dir_contents(src_path, dest_path);
3960+
if (copy_dir_contents(src_path, dest_path) && !dest_existed) {
3961+
std::filesystem::permissions(
3962+
dest_path, std::filesystem::status(src_path).permissions(), error);
3963+
if (error) {
3964+
return env->ThrowStdErrException(error, "cp", *dest);
3965+
}
3966+
}
39433967
}
39443968

39453969
BindingData::FilePathIsFileReturnType BindingData::FilePathIsFile(
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// This tests that cpSync gives the directories it creates the mode of the
2+
// corresponding source directory, as cp does.
3+
import { mustNotMutateObjectDeep, isWindows, skip } from '../common/index.mjs';
4+
import { nextdir } from '../common/fs.js';
5+
import assert from 'node:assert';
6+
import { chmodSync, cpSync, mkdirSync, statSync, writeFileSync, promises } from 'node:fs';
7+
import { join } from 'node:path';
8+
import { isMainThread } from 'node:worker_threads';
9+
import tmpdir from '../common/tmpdir.js';
10+
11+
if (isWindows)
12+
skip('directory modes are not meaningful on Windows');
13+
if (!isMainThread)
14+
skip('process.umask() is not available in workers');
15+
16+
tmpdir.refresh();
17+
const mask = process.umask(0o022);
18+
19+
const src = nextdir();
20+
mkdirSync(join(src, 'private', 'inner'), { recursive: true, mode: 0o700 });
21+
mkdirSync(join(src, 'shared'), { mode: 0o775 });
22+
writeFileSync(join(src, 'private', 'inner', 'file'), 'x', { mode: 0o600 });
23+
24+
function modes(root) {
25+
return ['.', 'private', 'private/inner', 'shared', 'private/inner/file']
26+
.map((p) => (statSync(join(root, p)).mode & 0o777).toString(8));
27+
}
28+
29+
const destSync = nextdir();
30+
cpSync(src, destSync, mustNotMutateObjectDeep({ recursive: true }));
31+
assert.deepStrictEqual(modes(destSync), modes(src));
32+
33+
const destAsync = nextdir();
34+
await promises.cp(src, destAsync, { recursive: true });
35+
assert.deepStrictEqual(modes(destAsync), modes(src));
36+
37+
// A read-only source directory can still be copied; its copy ends up read-only too.
38+
{
39+
const roSrc = nextdir();
40+
mkdirSync(join(roSrc, 'sub'), { recursive: true });
41+
writeFileSync(join(roSrc, 'sub', 'file'), 'x');
42+
chmodSync(join(roSrc, 'sub'), 0o555);
43+
chmodSync(roSrc, 0o555);
44+
const readOnly = [roSrc, join(roSrc, 'sub')];
45+
for (const copy of [(dest) => cpSync(roSrc, dest, { recursive: true }),
46+
(dest) => promises.cp(roSrc, dest, { recursive: true })]) {
47+
const dest = nextdir();
48+
await copy(dest);
49+
assert.strictEqual(statSync(join(dest, 'sub', 'file')).size, 1);
50+
assert.deepStrictEqual(
51+
[dest, join(dest, 'sub')].map((p) => (statSync(p).mode & 0o777).toString(8)), ['555', '555']);
52+
readOnly.push(dest, join(dest, 'sub'));
53+
}
54+
// Let tmpdir clean up.
55+
for (const dir of readOnly) chmodSync(dir, 0o755);
56+
}
57+
58+
// An existing destination directory keeps its own mode.
59+
const existing = nextdir();
60+
mkdirSync(existing, { mode: 0o711 });
61+
cpSync(src, existing, mustNotMutateObjectDeep({ recursive: true }));
62+
assert.strictEqual((statSync(existing).mode & 0o777).toString(8), '711');
63+
assert.deepStrictEqual(modes(existing).slice(1), modes(src).slice(1));
64+
process.umask(mask);

0 commit comments

Comments
 (0)