Merge branch 'fix/parity-chmod' into feat/rsync-parity

This commit is contained in:
2026-09-16 01:24:05 +02:00
13 changed files with 513 additions and 184 deletions
+127
View File
@@ -936,6 +936,133 @@ class TestChmod:
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
assert (os.stat(os.path.join(received, "small.txt")).st_mode & 0o777) == 0o644
@pytest.mark.ci
def test_chmod_does_not_imply_perms(self, shared_server):
"""rsync's --chmod only tweaks the mode used for a NEW destination; it
does not imply -p, so a pre-existing destination keeps its own mode."""
source = os.path.join(TEST_DATA_DIR, "chmod_nop_src")
dest = os.path.join(TEST_DATA_DIR, "chmod_nop_dst")
clean_dir(source)
clean_dir(dest)
src_file = os.path.join(source, "f.txt")
with open(src_file, "wb") as fh:
fh.write(b"one\n")
os.chmod(src_file, 0o644)
result, _ = run_client(source, dest, flags=["-p"], port=shared_server.port)
assert result.returncode == 0, f"seed failed: {(result.stderr or '')[:200]}"
dst_file = os.path.join(get_dest_received_dir(dest, source), "f.txt")
os.chmod(dst_file, 0o600)
with open(src_file, "wb") as fh:
fh.write(b"two, changed content\n")
result, _ = run_client(source, dest, flags=["--chmod=go+w"],
port=shared_server.port)
assert result.returncode == 0, \
f"--chmod failed: {(result.stderr or result.stdout)[:300]}"
got = stat.S_IMODE(os.stat(dst_file).st_mode)
assert got == 0o600, \
f"--chmod must not imply -p; existing dest mode changed to {oct(got)}"
@pytest.mark.ci
def test_chmod_go_w_with_perms(self, shared_server):
source = os.path.join(TEST_DATA_DIR, "chmod_gow_src")
dest = os.path.join(TEST_DATA_DIR, "chmod_gow_dst")
clean_dir(source)
clean_dir(dest)
src_file = os.path.join(source, "f.txt")
with open(src_file, "wb") as fh:
fh.write(b"x\n")
os.chmod(src_file, 0o644)
result, _ = run_client(source, dest, flags=["-p", "--chmod=go+w"],
port=shared_server.port)
assert result.returncode == 0, \
f"-p --chmod=go+w failed: {(result.stderr or result.stdout)[:300]}"
got = stat.S_IMODE(os.stat(
os.path.join(get_dest_received_dir(dest, source), "f.txt")).st_mode)
assert got == 0o666, f"--chmod=go+w must grant group/other write, got {oct(got)}"
@pytest.mark.ci
def test_chmod_repeated_options_accumulate(self, shared_server):
source = os.path.join(TEST_DATA_DIR, "chmod_append_src")
dest = os.path.join(TEST_DATA_DIR, "chmod_append_dst")
clean_dir(source)
clean_dir(dest)
src_file = os.path.join(source, "f.txt")
with open(src_file, "wb") as fh:
fh.write(b"x\n")
os.chmod(src_file, 0o644)
result, _ = run_client(source, dest,
flags=["-p", "--chmod=a+r", "--chmod=a-w"],
port=shared_server.port)
assert result.returncode == 0, \
f"append --chmod failed: {(result.stderr or result.stdout)[:300]}"
got = stat.S_IMODE(os.stat(
os.path.join(get_dest_received_dir(dest, source), "f.txt")).st_mode)
assert got == 0o444, f"repeated --chmod must accumulate, got {oct(got)}"
@pytest.mark.ci
@pytest.mark.skipif(shutil.which("rsync") is None, reason="rsync not installed")
def test_chmod_matches_rsync(self, shared_server):
"""Differential --chmod verification against rsync 3.4.1 for D/F/X
selectors, no-/with--p new files, special bits, and append semantics."""
cases = [
("go_w_no_p", ["--chmod=go+w"], {}, {"f.txt": (b"x", 0o644)}, ["f.txt"]),
("go_w_p", ["-p", "--chmod=go+w"], {}, {"f.txt": (b"x", 0o644)}, ["f.txt"]),
("world_writable_p", ["-p"], {}, {"f.txt": (b"x", 0o666)}, ["f.txt"]),
("setgid_sticky_dirs_p", ["-p"], {"sg": 0o2755, "st": 0o1777},
{"sg/a.txt": (b"x", 0o644), "st/b.txt": (b"x", 0o644)},
["sg", "st"]),
("special_file_p", ["-p"], {}, {"s": (b"x", 0o6755)}, ["s"]),
("archive_special_file", ["-a"], {}, {"s": (b"x", 0o6755)}, ["s"]),
("archive_setgid_dir", ["-a"], {"d": 0o2755},
{"d/a.txt": (b"x", 0o644)}, ["d"]),
("dfx_p", ["-p", "--chmod=Dg+s,Fo-w,+X"], {"d": 0o700},
{"d/inner.txt": (b"x", 0o644), "f.txt": (b"x", 0o644)}, ["d", "f.txt"]),
("x_selector_p", ["-p", "--chmod=a+X"], {"d": 0o600},
{"d/inner.txt": (b"x", 0o644), "exe": (b"x", 0o755), "noexe": (b"x", 0o644)},
["d", "exe", "noexe"]),
("append_p", ["-p", "--chmod=a+r", "--chmod=a-w"], {},
{"f.txt": (b"x", 0o644)}, ["f.txt"]),
]
for name, flags, dirs, files, check in cases:
source = os.path.join(TEST_DATA_DIR, f"chmod_diff_{name}_src")
fdest = os.path.join(TEST_DATA_DIR, f"chmod_diff_{name}_fs")
rdest = os.path.join(TEST_DATA_DIR, f"chmod_diff_{name}_rsync")
clean_dir(source)
clean_dir(fdest)
clean_dir(rdest)
for rel, mode in dirs.items():
path = os.path.join(source, rel)
os.makedirs(path, exist_ok=True)
os.chmod(path, mode)
for rel, (content, mode) in files.items():
path = os.path.join(source, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fh:
fh.write(content)
os.chmod(path, mode)
rsync_result = subprocess.run(
["rsync", "-r"] + flags + [source + "/", rdest + "/"],
text=True, capture_output=True)
assert rsync_result.returncode == 0, \
f"rsync {name} failed: {rsync_result.stderr[:300]}"
result, _ = run_client(source, fdest, flags=flags, port=shared_server.port)
assert result.returncode == 0, \
f"FastSync {name} failed: {(result.stderr or result.stdout)[:300]}"
fs_root = get_dest_received_dir(fdest, source)
for rel in check:
rsync_mode = stat.S_IMODE(os.lstat(os.path.join(rdest, rel)).st_mode)
fs_mode = stat.S_IMODE(os.lstat(os.path.join(fs_root, rel)).st_mode)
assert fs_mode == rsync_mode, (
f"{name}: mode mismatch for {rel}: "
f"FastSync {oct(fs_mode)} != rsync {oct(rsync_mode)}")
class TestPreallocate:
"""--preallocate allocates the destination file space up front; the final
+20 -23
View File
@@ -95,14 +95,14 @@ class TestPreservePerms:
source = os.path.join(TEST_DATA_DIR, "perms_nop_new_src")
dest = os.path.join(TEST_DATA_DIR, "perms_nop_new_dst")
# 0664 has group/other bits that the umask strips, so the result is not
# just the source mode. FastSync additionally never grants group/other
# write from a client-supplied mode (S_IWGRP|S_IWOTH are always
# cleared), so the expected mode masks those too.
# just the source mode. Under strict rsync parity the source mode is
# masked only by the umask (group/other write is no longer force-cleared
# on top of it).
_seed_file(source, dest, "f.txt", b"new\n", 0o664)
result, _ = run_client(source, dest, flags=["-t"], port=shared_server.port)
assert result.returncode == 0, f"-t failed: {(result.stderr or '')[:300]}"
want = 0o664 & ~_process_umask() & ~0o022
assert result.returncode == 0, f"-t failed: {(result.stderr or result.stdout)[:300]}"
want = 0o664 & ~_process_umask()
got = os.stat(_received(dest, source, "f.txt")).st_mode & 0o777
assert got == want, \
f"new no--p destination mode: want {oct(want)}, got {oct(got)}"
@@ -254,15 +254,15 @@ class TestDirectoryModes:
assert got == 0o750, f"-p must apply the source directory mode, got {oct(got)}"
@pytest.mark.ci
def test_p_sanitizes_directory_group_other_write(self, shared_server):
# A 0777 source directory must never produce a group/other-writable
# destination directory: the file-mode sanitization is applied to dirs.
source, dest, _ = self._tree("dirmode_sanitize", 0o777, pin_mtime=False)
def test_p_preserves_directory_group_other_write(self, shared_server):
# Strict rsync parity: -p copies the source directory mode exactly,
# including group/other write (the old sanitization is gone).
source, dest, _ = self._tree("dirmode_go_write", 0o777, pin_mtime=False)
result, _ = run_client(source, dest, flags=["-p"], port=shared_server.port)
assert result.returncode == 0, f"-p failed: {(result.stderr or result.stdout)[:300]}"
mode = os.stat(os.path.join(get_dest_received_dir(dest, source), "sub")).st_mode & 0o777
assert mode & 0o022 == 0, \
f"directory must never be group/other writable, got {oct(mode)}"
assert mode == 0o777, \
f"-p must preserve the source directory mode exactly, got {oct(mode)}"
@pytest.mark.ci
def test_omit_dir_times_suppresses_times_not_modes(self, shared_server):
@@ -443,14 +443,13 @@ class TestPreserveFeatureMatrix:
class TestSpecialNodeModes:
"""Security: a client can never grant group/other write, including on a
recreated special node (FIFO). The special-node creation path sanitizes
S_IWGRP|S_IWOTH just like the regular-file and directory paths, so a source
FIFO with mode 0777 must land as 0755 (owner/group/other read+exec from the
source otherwise preserved). FIFOs are created unprivileged via mkfifo."""
"""Strict rsync parity: with -p the source FIFO mode is copied exactly,
including group/other write. Without -p the node follows the same
source & ~umask base as any other new entry. FIFOs are created
unprivileged via mkfifo."""
@pytest.mark.ci
def test_specials_p_sanitizes_fifo_group_other_write(self):
def test_specials_p_preserves_fifo_mode(self):
source = os.path.join(TEST_DATA_DIR, "specialmode_src")
dest = os.path.join(TEST_DATA_DIR, "specialmode_dst")
clean_dir(source)
@@ -464,8 +463,8 @@ class TestSpecialNodeModes:
# Production daemonizes with umask(0) (server.c) so the source mode is
# what reaches mkfifo. The session server runs in the foreground and
# would inherit the runner's umask, which alone would strip the write
# bits and mask a regression in the sanitization. Start a dedicated
# foreground server under umask(0) to exercise the real path.
# bits and mask a regression. Start a dedicated foreground server under
# umask(0) to exercise the real path.
server = ServerManager()
saved_umask = os.umask(0)
try:
@@ -486,7 +485,5 @@ class TestSpecialNodeModes:
st = os.lstat(received)
assert stat.S_ISFIFO(st.st_mode), f"received entry is not a FIFO: {oct(st.st_mode)}"
mode = st.st_mode & 0o777
assert mode & 0o022 == 0, \
f"recreated FIFO must never be group/other writable, got {oct(mode)}"
assert mode == 0o755, \
f"-p must preserve the source FIFO mode minus group/other write (want 0o755), got {oct(mode)}"
assert mode == 0o777, \
f"-p must preserve the source FIFO mode exactly (want 0o777), got {oct(mode)}"
+32 -3
View File
@@ -530,7 +530,9 @@ static void test_parse_args_chmod() {
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->chmod_spec, "u=rw,go=r");
EXPECT_TRUE(cfg->preserve_perms);
/* rsync's --chmod does NOT imply --perms: it only tweaks the mode used for a
* new destination unless -p is also given. */
EXPECT_FALSE(cfg->preserve_perms);
EXPECT_TRUE(cfg->use_metadata);
mode_t result;
EXPECT_TRUE(chmod_apply(0777, cfg->chmod_spec, &result));
@@ -545,14 +547,39 @@ static void test_parse_args_numeric_chmod() {
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->chmod_spec, "7777");
EXPECT_TRUE(cfg->preserve_perms);
EXPECT_FALSE(cfg->preserve_perms);
EXPECT_TRUE(cfg->use_metadata);
config_delete(cfg);
}
static void test_parse_args_accepts_selector_chmod() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--chmod=Dg+s,Fo-w,+X", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->chmod_spec, "Dg+s,Fo-w,+X");
EXPECT_FALSE(cfg->preserve_perms);
config_delete(cfg);
}
static void test_parse_args_appends_repeated_chmod() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--chmod=a+r", "--chmod=a-w", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
/* Repeated --chmod options accumulate (rsync >= 3.2.4) instead of replacing. */
EXPECT_EQ_STR(cfg->chmod_spec, "a+r,a-w");
mode_t result;
EXPECT_TRUE(chmod_apply(0644, cfg->chmod_spec, &result));
EXPECT_EQ_INT(result, 0444);
config_delete(cfg);
}
static void test_parse_args_rejects_invalid_chmod() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--chmod=a+X", "/src", "/dst"};
char* argv[] = {"fastsync", "--chmod=a+r,", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
@@ -4213,6 +4240,8 @@ void test_client_cli() {
test_parse_args_executability();
test_parse_args_chmod();
test_parse_args_numeric_chmod();
test_parse_args_accepts_selector_chmod();
test_parse_args_appends_repeated_chmod();
test_parse_args_rejects_invalid_chmod();
test_parse_args_invalid_port();
test_parse_args_non_numeric_port();
+26 -32
View File
@@ -1007,7 +1007,8 @@ static void test_inplace_overwrite_metadata_strips_special_bits() {
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
/* Metadata-derived mode is applied and never includes setuid/setgid/sticky. */
/* No -p: the pre-existing destination mode (without its special bits) is
* restored; the source mode is not applied. */
EXPECT_EQ_INT((int)(st.st_mode & (S_ISUID | S_ISGID | S_ISVTX)), 0);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0755);
@@ -1089,12 +1090,11 @@ static void test_atomic_no_perms_preserves_destination_mode() {
unlink(fresh);
}
/* MAJOR 2: a brand-new destination file must never be created group/other
* writable from a client-supplied source mode. The daemon runs with umask(0),
* so without the explicit S_IWGRP|S_IWOTH strip a source 0666 (with no -p)
* would materialize as world-writable. */
static void test_new_file_mode_never_group_other_writable() {
const char* path = "test_new_file_no_go_write.bin";
/* Strict rsync parity: a brand-new destination file with no -p follows
* rsync's source_mode & ~umask base, so group/other write in the source mode is
* honored exactly as the umask allows (it is no longer force-cleared). */
static void test_new_file_mode_honors_source_and_umask() {
const char* path = "test_new_file_mode.bin";
unlink(path);
FileMetadata m;
memset(&m, 0, sizeof(m));
@@ -1108,19 +1108,15 @@ static void test_new_file_mode_never_group_other_writable() {
EXPECT_TRUE(ok);
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT((int)(st.st_mode & (S_IWGRP | S_IWOTH)), 0);
/* The rest of the source mode is still honored (owner write survives). */
EXPECT_EQ_INT((int)(st.st_mode & S_IWUSR), S_IWUSR);
EXPECT_EQ_INT((int)(st.st_mode & 0777), (int)(0666 & ~(mode_t)file_process_umask()));
unlink(path);
}
/* Security: a client-supplied special-node mode must never materialize a
* group/other-writable FIFO. file_save_special_to_disk() sanitizes the
* creation bits the same way the regular-file policy does: under -p the source
* mode loses S_IWGRP|S_IWOTH (0777 -> 0755), and without -p a safe 0644 default
* is used. The daemon runs with umask(0) (server.c), so the explicit strip is
* what keeps the node safe -- the test clears the umask to prove it. */
static void test_special_fifo_mode_never_group_other_writable_impl() {
/* Strict rsync parity for recreated special nodes: with -p the source mode is
* copied exactly (0777 -> 0777), and without -p the same source & ~umask base
* as any other new entry applies. The process umask is cleared so the source
* bits are what reaches mkfifo. */
static void test_special_fifo_mode_honors_source_and_umask_impl() {
const char* root = "test_special_mode_tmp";
const char* with_p = "test_special_mode_tmp/with_p.fifo";
const char* no_p = "test_special_mode_tmp/no_p.fifo";
@@ -1139,7 +1135,7 @@ static void test_special_fifo_mode_never_group_other_writable_impl() {
meta.gid = getegid();
meta.mtime_sec = 1000000000;
/* -p: the source mode is honored minus group/other write. */
/* -p: the source mode (including group/other write) is copied exactly. */
File* f = file_create("with_p.fifo");
EXPECT_NOT_NULL(f);
f->is_special = true;
@@ -1151,12 +1147,11 @@ static void test_special_fifo_mode_never_group_other_writable_impl() {
struct stat st;
EXPECT_EQ_INT(lstat(with_p, &st), 0);
EXPECT_TRUE(S_ISFIFO(st.st_mode));
EXPECT_EQ_INT((int)(st.st_mode & (S_IWGRP | S_IWOTH)), 0);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0755);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0777);
f->metadata = NULL;
file_destroy(f);
/* No -p: the fixed safe default, never the source's 0777. */
/* No -p: source & ~umask (umask is cleared, so 0777). */
f = file_create("no_p.fifo");
EXPECT_NOT_NULL(f);
f->is_special = true;
@@ -1165,8 +1160,7 @@ static void test_special_fifo_mode_never_group_other_writable_impl() {
EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN);
EXPECT_EQ_INT(lstat(no_p, &st), 0);
EXPECT_TRUE(S_ISFIFO(st.st_mode));
EXPECT_EQ_INT((int)(st.st_mode & (S_IWGRP | S_IWOTH)), 0);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0644);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0777);
f->metadata = NULL;
file_destroy(f);
@@ -1176,15 +1170,15 @@ static void test_special_fifo_mode_never_group_other_writable_impl() {
rmdir(root);
}
/* The receiver daemon runs umask(0), so an unsanitized source mode would reach
* mkfifo unmasked. Run the body with umask(0) to exercise the explicit strip,
* and restore the process umask from this wrapper so a failing EXPECT inside the
* body (which returns from the body only) cannot leak umask(0) into later
* tests. */
static void test_special_fifo_mode_never_group_other_writable() {
/* The receiver daemon runs umask(0), so the source mode reaches mkfifo
* unmasked. Run the body with umask(0) and refresh the cached process umask so
* file_process_umask() agrees, then restore both. */
static void test_special_fifo_mode_honors_source_and_umask() {
mode_t saved_umask = umask(0);
test_special_fifo_mode_never_group_other_writable_impl();
file_umask_capture();
test_special_fifo_mode_honors_source_and_umask_impl();
umask(saved_umask);
file_umask_capture();
}
/* --specials recreates a unix-domain socket via mknod(S_IFSOCK), which Linux
@@ -2056,8 +2050,8 @@ void test_file() {
test_inplace_overwrite_clears_special_mode_bits();
test_inplace_overwrite_metadata_strips_special_bits();
test_atomic_no_perms_preserves_destination_mode();
test_new_file_mode_never_group_other_writable();
test_special_fifo_mode_never_group_other_writable();
test_new_file_mode_honors_source_and_umask();
test_special_fifo_mode_honors_source_and_umask();
test_special_socket_recreated();
test_inplace_overwrite_truncates_shorter_payload();
test_inplace_refuses_fifo_destination();
+70 -5
View File
@@ -448,9 +448,9 @@ static void test_file_restore_executability_rsync_rule() {
/* The shared metadata_mode_for_policy() helper is the single source of truth
* used by both the normal metadata path and the --fake-super replay. It must
* reproduce the per-attribute split: no mode change when neither -p nor -E is
* set; -p applies the sanitized source mode (group/other write cleared)
* regardless of the destination; -E derives exec bits from the destination and
* --perms wins when both are set. */
* set; -p applies the source mode exactly (including group/other write and the
* setuid/setgid/sticky bits) regardless of the destination; -E derives exec
* bits from the destination and --perms wins when both are set. */
static void test_metadata_mode_for_policy() {
mode_t out = 0xdead;
EXPECT_FALSE(
@@ -459,7 +459,13 @@ static void test_metadata_mode_for_policy() {
EXPECT_TRUE(
metadata_mode_for_policy(0777, 0644, (FileAttrPolicy){true, false, false, false}, &out));
EXPECT_EQ_INT((int)(out & 0777), 0755); /* group/other write always cleared */
EXPECT_EQ_INT((int)(out & 0777), 0777); /* group/other write is preserved */
mode_t specials = (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0672);
EXPECT_TRUE(
metadata_mode_for_policy(specials, 0644, (FileAttrPolicy){true, false, false, false}, &out));
EXPECT_EQ_INT((int)(out & (S_ISUID | S_ISGID | S_ISVTX | 0777)),
(int)(S_ISUID | S_ISGID | S_ISVTX | 0672));
/* -E: exec bits derive from the DESTINATION's read bits. */
EXPECT_TRUE(
@@ -566,6 +572,27 @@ static void test_file_attr_policy_from_config() {
config_delete(c);
}
/* Strict rsync parity: -p copies the source's setuid/setgid/sticky bits (they
* are attempted, not masked away). On Linux these are settable on a file the
* receiving user owns; a mount that denies them would log a chmod failure. */
static void test_perms_preserves_special_bits() {
const char* path = "temp_special_bits.txt";
unlink(path);
FileMetadata m = {
.mode = (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0755), .uid = getuid(), .gid = getgid()};
bool ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
(FileAttrPolicy){true, false, false, false}, false, false,
false, NULL, false, false, NULL);
EXPECT_TRUE(ok);
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0755);
EXPECT_EQ_INT((int)(st.st_mode & (S_ISUID | S_ISGID | S_ISVTX)),
(int)(S_ISUID | S_ISGID | S_ISVTX));
unlink(path);
}
static void test_chmod_changes() {
mode_t result;
EXPECT_TRUE(chmod_apply(0777, "u=rw,go=r", &result));
@@ -583,8 +610,45 @@ static void test_chmod_changes() {
EXPECT_EQ_INT(result, 0755);
EXPECT_FALSE(chmod_apply(0777, "888", &result));
EXPECT_FALSE(chmod_apply(0777, "10000", &result));
EXPECT_FALSE(chmod_apply(0777, "a+X", &result));
EXPECT_FALSE(chmod_apply(0777, "a+r,", &result));
/* go+w is honored (rsync gives 0666 from a 0644 file). */
EXPECT_TRUE(chmod_apply(0644, "go+w", &result));
EXPECT_EQ_INT(result, 0666);
/* X only sets execute on directories or already-executable files. */
EXPECT_TRUE(chmod_apply(0644, "a+X", &result));
EXPECT_EQ_INT(result, 0644);
EXPECT_TRUE(chmod_apply(0755, "a+X", &result));
EXPECT_EQ_INT(result, 0755);
EXPECT_TRUE(chmod_apply((mode_t)(S_IFDIR | 0644), "a+X", &result));
EXPECT_EQ_INT((int)(result & 0777), 0755);
EXPECT_TRUE(S_ISDIR(result));
/* D/F selectors restrict a clause to directories/files. */
EXPECT_TRUE(chmod_apply((mode_t)(S_IFDIR | 0700), "Dg+s", &result));
EXPECT_EQ_INT((int)(result & 07777), 02700);
EXPECT_TRUE(chmod_apply((mode_t)(S_IFREG | 0644), "Dg+s", &result));
EXPECT_EQ_INT((int)(result & 07777), 0644);
EXPECT_TRUE(chmod_apply((mode_t)(S_IFREG | 0644), "Fo-w", &result));
EXPECT_EQ_INT((int)(result & 07777), 0644);
EXPECT_TRUE(chmod_apply((mode_t)(S_IFREG | 0666), "Fo-w", &result));
EXPECT_EQ_INT((int)(result & 07777), 0664);
EXPECT_TRUE(chmod_apply((mode_t)(S_IFDIR | 0666), "Fo-w", &result));
EXPECT_EQ_INT((int)(result & 07777), 0666);
EXPECT_FALSE(chmod_apply(0644, "DFu+w", &result));
/* Special bits: s/t map to setuid/setgid/sticky like rsync. */
EXPECT_TRUE(chmod_apply(0755, "u+s", &result));
EXPECT_EQ_INT((int)(result & 07777), 04755);
EXPECT_TRUE(chmod_apply(0755, "g+s", &result));
EXPECT_EQ_INT((int)(result & 07777), 02755);
EXPECT_TRUE(chmod_apply(0755, "a+t", &result));
EXPECT_EQ_INT((int)(result & 07777), 01755);
/* Comma-separated clauses accumulate (the CLI joins repeated options). */
EXPECT_TRUE(chmod_apply(0644, "g+w,u+x", &result));
EXPECT_EQ_INT((int)(result & 07777), 0764);
}
/* P7 Wave D: symlink metadata is applied with no-follow primitives, and -J
@@ -722,5 +786,6 @@ void test_metadata() {
test_file_restore_metadata_fd_attribute_split();
test_file_attr_policy_from_config();
test_file_restore_symlink_metadata();
test_perms_preserves_special_bits();
test_chmod_changes();
}
+3 -4
View File
@@ -377,13 +377,12 @@ static void test_fake_super_restore() {
EXPECT_EQ_INT(fstat(fd, &st), 0);
EXPECT_EQ_INT((int)(st.st_mode & 07777), 0751);
/* Mode sanitization: the normal metadata path never grants group/other write
bits, and fake-super replay must not re-add them (a recorded 0666 restores
as 0644, never as world-writable). */
/* Strict rsync parity: -p restores the recorded mode exactly, including
group/other write (a recorded 0666 restores as 0666). */
fake_super_store_fd(fd, 1001, 1002, 0666, 1700000000, 0);
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
EXPECT_EQ_INT(fstat(fd, &st), 0);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0644);
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0666);
/* Restore with a malformed record must skip without failing. */
time_t before = st.st_mtime;