Merge branch 'feat/p7-copy-as' into feat/p7-privilege

# Conflicts:
#	RSYNC_COMPAT.md
#	src/shared/config.c
#	src/shared/config.h
#	src/shared/identity.c
#	tests/integration/test_preflight.py
#	tests/test_client_cli.c
#	tests/test_config.c
This commit is contained in:
2026-09-12 12:14:44 +02:00
10 changed files with 510 additions and 52 deletions
+73 -2
View File
@@ -243,8 +243,8 @@ static void test_parse_args_protocol_accept_current() {
/* Any --protocol value other than the current PROTOCOL_VERSION must end in
* failure (parse_args simply stores it; validate_config rejects it up front). */
static void test_parse_args_protocol_rejects_other_versions() {
static const char* const bad_versions[] = {"2.17", "2.16", "2.15.0", "2.16.0", "2.17.0",
"216", "31", "abc", ""};
static const char* const bad_versions[] = {"2.17", "2.16", "2.15.0", "2.16.0", "2.17.0",
"216", "31", "abc", ""};
for (size_t i = 0; i < sizeof(bad_versions) / sizeof(bad_versions[0]); i++) {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
@@ -2577,6 +2577,64 @@ static void test_parse_args_chown() {
config_delete(cfg);
}
/* --copy-as=USER[:GROUP] (P7 Wave E): resolve the user/group against the local
* databases, imply metadata, and apply the documented group-default rule. */
static void test_parse_args_copy_as() {
/* Explicit numeric user and group. */
Config* cfg = config_create();
char* argv[] = {"fastsync", "--copy-as=@1000:@1001", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->copy_as_set);
EXPECT_TRUE(cfg->use_metadata);
EXPECT_EQ_INT(cfg->copy_as_uid, 1000);
EXPECT_EQ_INT(cfg->copy_as_gid, 1001);
config_delete(cfg);
/* Space form. */
cfg = config_create();
positional_count = 0;
char* argv2[] = {"fastsync", "--copy-as", "@2000:3000", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->copy_as_uid, 2000);
EXPECT_EQ_INT(cfg->copy_as_gid, 3000);
config_delete(cfg);
/* Group omitted: a resolvable user uses its primary gid. */
struct passwd* self = getpwuid(geteuid());
if (self) {
cfg = config_create();
positional_count = 0;
char* argv3[] = {"fastsync", (char*)"--copy-as", (char*)self->pw_name, "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 5, argv3, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->copy_as_uid, (int32_t)self->pw_uid);
EXPECT_EQ_INT(cfg->copy_as_gid, (int32_t)self->pw_gid);
config_delete(cfg);
}
/* Group omitted with a numeric id that has no passwd entry: gid falls back
* to uid (documented divergence). */
if (!getpwuid((uid_t)4242)) {
cfg = config_create();
positional_count = 0;
char* argv4[] = {"fastsync", "--copy-as=@4242", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, argv4, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->copy_as_uid, 4242);
EXPECT_EQ_INT(cfg->copy_as_gid, 4242);
config_delete(cfg);
}
/* '*' means the client's current euid/egid. */
cfg = config_create();
positional_count = 0;
char* argv5[] = {"fastsync", "--copy-as=*:*", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, argv5, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->copy_as_uid, (int32_t)geteuid());
EXPECT_EQ_INT(cfg->copy_as_gid, (int32_t)getegid());
config_delete(cfg);
}
/* Malformed identity specs are rejected, never silently ignored. */
static void test_parse_args_rejects_malformed_identity() {
struct {
@@ -2590,6 +2648,12 @@ static void test_parse_args_rejects_malformed_identity() {
{"--groupmap", "no_such_group_qqq:x"},
{"--chown", "a:b:c"},
{"--chown", "no_such_user_zzz:"},
{"--copy-as", ""},
{"--copy-as", ":"},
{"--copy-as", "a:b:c"},
{"--copy-as", "@1000:"},
{"--copy-as", "definitely_not_a_real_user_zzz"},
{"--copy-as", "no_such_group_qqq_group"},
};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
Config* cfg = config_create();
@@ -2607,6 +2671,12 @@ static void test_parse_args_rejects_malformed_identity() {
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 2, argv, positional_args, &positional_count), -1);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* argv2[] = {"fastsync", "--copy-as"};
EXPECT_EQ_INT(parse_args(cfg, 2, argv2, positional_args, &positional_count), -1);
config_delete(cfg);
}
/* --preallocate parses as a boolean flag and validates cleanly. */
@@ -3010,6 +3080,7 @@ void test_client_cli() {
test_parse_args_groupmap();
test_parse_args_usermap_name_resolution();
test_parse_args_chown();
test_parse_args_copy_as();
test_parse_args_rejects_malformed_identity();
test_parse_args_preallocate();
test_parse_args_metadata_times();