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:
+237
-7
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user