feat: per-attribute preservation flags -p/-t/-o/-g with --no-* negations (protocol 2.22.0)

Split FastSync's single use_metadata bundle into four independent rsync-parity attributes: preserve_perms, preserve_times, preserve_owner, preserve_group. use_metadata is now a derived transport bit (config_derived_use_metadata).

CLI: real -p/--perms, -t/--times, -o/--owner, -g/--group plus --no-perms/--no-times/--no-owner/--no-group (short and long) and --no-preserve; -a is now rsync -rlptgoD; --preserve = -pt; -A implies -p; -X does not; --chmod implies -p; --usermap/--groupmap/--chown imply owner/group per side; --incremental/--delta still auto-preserve unless negated.

Receiver: per-attribute FileAttrPolicy gating for files, dirs (modes applied at end of transfer), symlinks and specials; rsync -E read-bit rule; new files get source_mode & ~umask sanitized (no group/other write); per-side identity resolution; deferred directory metadata; batch dir-metadata replay; daemon modules without 'client owner = yes' no longer refuse plain -a but force super off (no ownership) with a warning.

Wire: PROTOCOL_VERSION 2.21.0 -> 2.22.0 (four appended config bools, golden 653 / 95530566005420798). FileMetadata/chunk/batch framing unchanged. Docs/CHANGELOG/CMake updated to 2.22.0.
This commit is contained in:
2026-09-15 19:32:02 +02:00
parent b3f7cad4db
commit 34970b961c
35 changed files with 2523 additions and 485 deletions
+237 -7
View File
@@ -1,8 +1,10 @@
#include "test_metadata.h"
#include "chmod.h"
#include "identity.h"
#include "metadata.h"
#include "protocol.h"
#include "test_utils.h"
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
@@ -337,7 +339,7 @@ static void test_file_restore_metadata_applies_atime() {
m.crtime_sec = 0;
m.crtime_nsec = 0;
file_restore_metadata(path, &m, false);
file_restore_metadata(path, &m, (FileAttrPolicy){true, true, true, false});
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
@@ -376,7 +378,7 @@ static void test_file_restore_metadata() {
.atime_valid = false,
.crtime_valid = false};
file_restore_metadata(path, &m, false);
file_restore_metadata(path, &m, (FileAttrPolicy){true, true, false, false});
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
@@ -393,7 +395,7 @@ static void test_file_restore_executability_only() {
FileMetadata m = {
.mode = 0751, .uid = getuid(), .gid = getgid(), .mtime_sec = 0, .mtime_nsec = 0};
file_restore_metadata(path, &m, true);
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true});
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
@@ -407,14 +409,163 @@ static void test_directory_restore_executability_only() {
FileMetadata m = {
.mode = 0755, .uid = getuid(), .gid = getgid(), .mtime_sec = 0, .mtime_nsec = 0};
file_restore_metadata(path, &m, true);
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true});
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, 0711);
/* rsync -E: an executable source derives exec from the DESTINATION's read
* bits. A 0700 directory has read only for the owner, so only the owner
* gains exec -- the result stays 0700 (not 0711, the old per-class copy). */
EXPECT_EQ_INT(st.st_mode & 0777, 0700);
rmdir(path);
}
/* rsync 3.4 -E truth table (preserve_perms off), verified against rsync 3.4.1:
* (src,dest) -> result. A non-executable source clears every execute bit; an
* executable source sets a class's execute bit iff that class can read. */
static void test_file_restore_executability_rsync_rule() {
static const struct {
mode_t src;
mode_t dest;
mode_t want;
} cases[] = {
{0755, 0644, 0755}, {0755, 0600, 0700}, {0755, 0640, 0750}, {0755, 0666, 0777},
{0700, 0640, 0750}, {0111, 0644, 0755}, {0644, 0755, 0644}, {0644, 0600, 0600},
};
const char* path = "temp_exec_rsync_rule.txt";
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
EXPECT_TRUE(file_write_to_disk(path, "x", 1, false, false));
EXPECT_EQ_INT(chmod(path, cases[i].dest), 0);
FileMetadata m = {.mode = cases[i].src, .uid = getuid(), .gid = getgid()};
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true});
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, cases[i].want);
unlink(path);
}
}
/* 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. */
static void test_metadata_mode_for_policy() {
mode_t out = 0xdead;
EXPECT_FALSE(
metadata_mode_for_policy(0777, 0644, (FileAttrPolicy){false, false, false, false}, &out));
EXPECT_EQ_INT((int)out, 0xdead); /* untouched when no change is requested */
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 */
/* -E: exec bits derive from the DESTINATION's read bits. */
EXPECT_TRUE(
metadata_mode_for_policy(0755, 0644, (FileAttrPolicy){false, false, false, true}, &out));
EXPECT_EQ_INT((int)(out & 0777), 0755);
EXPECT_TRUE(
metadata_mode_for_policy(0644, 0755, (FileAttrPolicy){false, false, false, true}, &out));
EXPECT_EQ_INT((int)(out & 0777), 0644);
EXPECT_TRUE(
metadata_mode_for_policy(0755, 0600, (FileAttrPolicy){false, false, false, true}, &out));
EXPECT_EQ_INT((int)(out & 0777), 0700);
/* --perms wins over -E when both are set. */
EXPECT_TRUE(
metadata_mode_for_policy(0700, 0644, (FileAttrPolicy){true, false, false, true}, &out));
EXPECT_EQ_INT((int)(out & 0777), 0700);
}
/* A brand-new file with -p off is created like rsync: source_mode & 0777 &
* ~umask (when metadata is available). The -E rule is then layered on top. */
static void test_new_file_mode_from_source_and_umask() {
const char* path = "temp_new_file_base.txt";
unlink(path);
FileMetadata m = {.mode = 0751, .uid = getuid(), .gid = getgid()};
mode_t want = (mode_t)(0751 & 0777 & ~(mode_t)file_process_umask());
bool ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
(FileAttrPolicy){false, 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(st.st_mode & 0777, want);
unlink(path);
/* -E on top of the source&~umask base (src 0751, umask 022 -> 0751). */
ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
(FileAttrPolicy){false, false, false, true}, false, false, false,
NULL, false, false, NULL);
EXPECT_TRUE(ok);
EXPECT_EQ_INT(stat(path, &st), 0);
mode_t want_e =
(want & 0444) ? (mode_t)(want | ((want & 0444) >> 2)) : (mode_t)(want & ~(mode_t)0111);
EXPECT_EQ_INT(st.st_mode & 0777, want_e);
unlink(path);
}
/* The per-attribute split: -t/-U apply times without touching the mode; an
* all-off policy applies neither mode nor times. */
static void test_file_restore_attribute_split() {
const char* path = "temp_meta_split_test.txt";
EXPECT_TRUE(file_write_to_disk(path, "x", 1, false, false));
EXPECT_EQ_INT(chmod(path, 0640), 0);
FileMetadata m = {.mode = 0755,
.uid = getuid(),
.gid = getgid(),
.mtime_sec = 1234567890,
.mtime_nsec = 0,
.atime_valid = false,
.crtime_valid = false};
/* times only: mtime changes, mode stays 0640. */
file_restore_metadata(path, &m, (FileAttrPolicy){false, true, false, false});
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, 0640);
EXPECT_EQ_INT((int)st.st_mtime, 1234567890);
/* no attributes: neither mode nor mtime changes. */
EXPECT_EQ_INT(chmod(path, 0640), 0);
struct timespec ts[2] = {{.tv_sec = 1000000000, .tv_nsec = 0},
{.tv_sec = 1000000000, .tv_nsec = 0}};
EXPECT_EQ_INT(utimensat(AT_FDCWD, path, ts, 0), 0);
FileMetadata m2 = m;
m2.mode = 0700;
m2.mtime_sec = 1600000000;
file_restore_metadata(path, &m2, (FileAttrPolicy){false, false, false, false});
EXPECT_EQ_INT(stat(path, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, 0640);
EXPECT_EQ_INT((int)st.st_mtime, 1000000000);
unlink(path);
}
static void test_file_attr_policy_from_config() {
FileAttrPolicy none = file_attr_policy_from_config(NULL);
EXPECT_FALSE(none.perms);
EXPECT_FALSE(none.times);
EXPECT_FALSE(none.atimes);
EXPECT_FALSE(none.executability);
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->preserve_perms = true;
c->preserve_times = true;
c->preserve_atimes = true;
c->use_executability = true;
FileAttrPolicy p = file_attr_policy_from_config(c);
EXPECT_TRUE(p.perms);
EXPECT_TRUE(p.times);
EXPECT_TRUE(p.atimes);
EXPECT_TRUE(p.executability);
config_delete(c);
}
static void test_chmod_changes() {
mode_t result;
EXPECT_TRUE(chmod_apply(0777, "u=rw,go=r", &result));
@@ -455,7 +606,7 @@ static void test_file_restore_symlink_metadata() {
/* Positive path: a non-omitted apply stamps the link's own mtime. */
FileMetadata applied = {.mtime_sec = 1000000000, .mtime_nsec = 0};
file_restore_symlink_metadata(link, &applied, false);
file_restore_symlink_metadata(link, &applied, (FileAttrPolicy){false, true, false, false}, false);
struct stat st;
EXPECT_EQ_INT(lstat(link, &st), 0);
EXPECT_TRUE(S_ISLNK(st.st_mode));
@@ -464,7 +615,7 @@ static void test_file_restore_symlink_metadata() {
/* -J: a different time must be left untouched. */
FileMetadata newer = {.mtime_sec = 1234567890, .mtime_nsec = 0};
file_restore_symlink_metadata(link, &newer, true);
file_restore_symlink_metadata(link, &newer, (FileAttrPolicy){false, true, false, false}, true);
EXPECT_EQ_INT(lstat(link, &st), 0);
EXPECT_EQ_INT((int)st.st_mtime, (int)t1);
if (symlink_times_supported)
@@ -475,6 +626,79 @@ static void test_file_restore_symlink_metadata() {
rmdir(dir);
}
/* Per-attribute gating of the descriptor restore path: -p alone applies the
* mode, -t alone the mtime, -U alone the atime, and an all-off policy leaves
* the destination's mode and times exactly as they are. This pins the fd API
* the receiver actually uses (the path-based file_restore_metadata has its own
* split test). */
static void test_file_restore_metadata_fd_attribute_split() {
identity_clear_active(); /* no ownership policy leaking from a previous test */
const char* path = "temp_meta_fd_split_test.txt";
EXPECT_TRUE(file_write_to_disk(path, "x", 1, false, false));
EXPECT_EQ_INT(chmod(path, 0640), 0);
int fd = open(path, O_RDWR);
EXPECT_TRUE(fd >= 0);
/* cppcheck-suppress knownConditionTrueFalse -- EXPECT_TRUE above asserts,
but cppcheck cannot see through the macro; the guard is defensive. */
if (fd < 0) {
unlink(path);
return;
}
FileMetadata m = {.mode = 0755,
.uid = getuid(),
.gid = getgid(),
.mtime_sec = 1234567890,
.mtime_nsec = 0,
.atime_valid = true,
.atime_sec = 999999999,
.atime_nsec = 0,
.crtime_valid = false};
struct stat st;
struct stat before;
/* perms-only: mode applied, mtime untouched. */
EXPECT_EQ_INT(fstat(fd, &before), 0);
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){true, false, false, false}));
EXPECT_EQ_INT(fstat(fd, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, 0755);
EXPECT_EQ_INT((int)st.st_mtime, (int)before.st_mtime);
/* times-only: mtime applied, mode untouched. */
EXPECT_EQ_INT(chmod(path, 0600), 0);
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){false, true, false, false}));
EXPECT_EQ_INT(fstat(fd, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, 0600);
EXPECT_EQ_INT((int)st.st_mtime, 1234567890);
/* atime-only: atime applied, mtime and mode untouched. */
struct timespec reset[2] = {{.tv_sec = 1000000000, .tv_nsec = 0},
{.tv_sec = 1000000000, .tv_nsec = 0}};
EXPECT_EQ_INT(futimens(fd, reset), 0);
EXPECT_EQ_INT(fstat(fd, &before), 0);
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){false, false, true, false}));
EXPECT_EQ_INT(fstat(fd, &st), 0);
EXPECT_EQ_INT((int)st.st_atime, 999999999);
EXPECT_EQ_INT((int)st.st_mtime, (int)before.st_mtime);
EXPECT_EQ_INT(st.st_mode & 0777, 0600);
/* all-off: neither mode nor either time is touched. */
EXPECT_EQ_INT(chmod(path, 0640), 0);
EXPECT_EQ_INT(futimens(fd, reset), 0);
FileMetadata m2 = m;
m2.mode = 0700;
m2.mtime_sec = 1600000000;
m2.atime_sec = 1700000000;
EXPECT_TRUE(file_restore_metadata_fd(fd, &m2, (FileAttrPolicy){false, false, false, false}));
EXPECT_EQ_INT(fstat(fd, &st), 0);
EXPECT_EQ_INT(st.st_mode & 0777, 0640);
EXPECT_EQ_INT((int)st.st_mtime, 1000000000);
EXPECT_EQ_INT((int)st.st_atime, 1000000000);
close(fd);
unlink(path);
}
void test_metadata() {
test_metadata_to_from_buf_roundtrip();
test_metadata_to_buf_null();
@@ -491,6 +715,12 @@ void test_metadata() {
test_file_restore_metadata_applies_atime();
test_file_restore_executability_only();
test_directory_restore_executability_only();
test_file_restore_executability_rsync_rule();
test_metadata_mode_for_policy();
test_new_file_mode_from_source_and_umask();
test_file_restore_attribute_split();
test_file_restore_metadata_fd_attribute_split();
test_file_attr_policy_from_config();
test_file_restore_symlink_metadata();
test_chmod_changes();
}