Skip to content

Commit ae2da34

Browse files
committed
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>
1 parent 8af1821 commit ae2da34

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
@@ -4083,6 +4083,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
40834083
auto dest_path = dest.ToPath();
40844084

40854085
std::error_code error;
4086+
const bool dest_existed = std::filesystem::exists(dest_path, error);
40864087
std::filesystem::create_directories(dest_path, error);
40874088
if (error) {
40884089
return env->ThrowStdErrException(error, "cp", *dest);
@@ -4202,11 +4203,28 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
42024203
}
42034204
} else if (dir_entry.is_directory()) {
42044205
auto entry_dir_path = src / dir_entry.path().filename();
4205-
std::filesystem::create_directory(dest_file_path);
4206+
const bool created =
4207+
std::filesystem::create_directory(dest_file_path, error);
4208+
if (error) {
4209+
env->ThrowStdErrException(
4210+
error, "cp", ConvertPathToUTF8(dest_file_path).c_str());
4211+
return false;
4212+
}
42064213
auto success = copy_dir_contents(entry_dir_path, dest_file_path);
42074214
if (!success) {
42084215
return false;
42094216
}
4217+
// A directory created by the copy gets the mode of its source once
4218+
// its contents are in (the source may be read-only).
4219+
if (created) {
4220+
std::filesystem::permissions(
4221+
dest_file_path, dir_entry.status().permissions(), error);
4222+
if (error) {
4223+
env->ThrowStdErrException(
4224+
error, "cp", ConvertPathToUTF8(dest_file_path).c_str());
4225+
return false;
4226+
}
4227+
}
42104228
} else if (dir_entry.is_regular_file()) {
42114229
std::filesystem::copy_file(
42124230
dir_entry.path(), dest_file_path, file_copy_opts, error);
@@ -4231,7 +4249,13 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
42314249
return true;
42324250
};
42334251

4234-
copy_dir_contents(src_path, dest_path);
4252+
if (copy_dir_contents(src_path, dest_path) && !dest_existed) {
4253+
std::filesystem::permissions(
4254+
dest_path, std::filesystem::status(src_path).permissions(), error);
4255+
if (error) {
4256+
return env->ThrowStdErrException(error, "cp", *dest);
4257+
}
4258+
}
42354259
}
42364260

42374261
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)