Merge branch 'feat/parity-ownership' into feat/rsync-parity

# Conflicts:
#	src/shared/config.h
#	tests/test_config.c
This commit is contained in:
2026-09-15 23:24:58 +02:00
24 changed files with 1117 additions and 378 deletions
+81
View File
@@ -1885,6 +1885,75 @@ static bool cli_handle_checksum_options(CliParseCtx* ctx) {
return false;
}
/* Determine whether a --chown spec sets the owner and/or group side, honoring
* the same escape-aware splitting as identity_parse_chown(): a `\:` is a literal
* colon, not a field separator. */
static void chown_spec_sides(const char* value, bool* has_owner, bool* has_group) {
*has_owner = false;
*has_group = false;
if (!value)
return;
bool split = false;
for (const char* p = value; *p; p++) {
if (*p == '\\' && p[1] == ':') {
p++;
continue;
}
if (*p == ':') {
split = true;
continue;
}
if (split)
*has_group = true;
else
*has_owner = true;
}
}
/* rsync refuses to mix --chown with --usermap/--groupmap on the SAME side
* ("--usermap conflicts with prior --chown"). `chown_value` is non-NULL only
* for the --chown option itself. Returns true and records a parse error when
* the new option conflicts with one already seen. */
static bool mapping_option_conflicts(CliParseCtx* ctx, const char* optname, bool is_group,
const char* chown_value) {
const Config* config = ctx->config;
if (chown_value) {
bool has_owner;
bool has_group;
chown_spec_sides(chown_value, &has_owner, &has_group);
if (has_owner && config->usermap_count > 0) {
log_message(LOG_LEVEL_ERROR, "%s conflicts with prior --usermap", optname);
ctx->exit_code = -1;
return true;
}
if (has_group && config->groupmap_count > 0) {
log_message(LOG_LEVEL_ERROR, "%s conflicts with prior --groupmap", optname);
ctx->exit_code = -1;
return true;
}
return false;
}
if (!is_group && config->chown_uid_set) {
log_message(LOG_LEVEL_ERROR, "%s conflicts with prior --chown", optname);
ctx->exit_code = -1;
return true;
}
if (is_group && config->chown_gid_set) {
log_message(LOG_LEVEL_ERROR, "%s conflicts with prior --chown", optname);
ctx->exit_code = -1;
return true;
}
return false;
}
static bool usermap_conflicts_with_chown(CliParseCtx* ctx, const char* optname, bool is_group) {
return mapping_option_conflicts(ctx, optname, is_group, NULL);
}
static bool chown_conflicts_with_map(CliParseCtx* ctx, const char* optname, const char* value) {
return mapping_option_conflicts(ctx, optname, false, value);
}
/* Remote-option, basis-directory and identity-mapping options. Returns true
* when the argument was consumed. */
static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
@@ -1957,6 +2026,8 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
return true;
}
if (strncmp(arg, "--usermap=", 10) == 0) {
if (usermap_conflicts_with_chown(ctx, "--usermap", false))
return true;
if (identity_parse_map(config, arg + 10, false) != 0) {
ctx->exit_code = -1;
return true;
@@ -1970,6 +2041,8 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
ctx->exit_code = -1;
return true;
}
if (usermap_conflicts_with_chown(ctx, "--usermap", false))
return true;
if (identity_parse_map(config, ctx->argv[++ctx->i], false) != 0) {
ctx->exit_code = -1;
return true;
@@ -1978,6 +2051,8 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
return true;
}
if (strncmp(arg, "--groupmap=", 11) == 0) {
if (usermap_conflicts_with_chown(ctx, "--groupmap", true))
return true;
if (identity_parse_map(config, arg + 11, true) != 0) {
ctx->exit_code = -1;
return true;
@@ -1991,6 +2066,8 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
ctx->exit_code = -1;
return true;
}
if (usermap_conflicts_with_chown(ctx, "--groupmap", true))
return true;
if (identity_parse_map(config, ctx->argv[++ctx->i], true) != 0) {
ctx->exit_code = -1;
return true;
@@ -1999,6 +2076,8 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
return true;
}
if (strncmp(arg, "--chown=", 8) == 0) {
if (chown_conflicts_with_map(ctx, "--chown", arg + 8))
return true;
if (identity_parse_chown(config, arg + 8) != 0) {
ctx->exit_code = -1;
return true;
@@ -2015,6 +2094,8 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
ctx->exit_code = -1;
return true;
}
if (chown_conflicts_with_map(ctx, "--chown", ctx->argv[ctx->i + 1]))
return true;
if (identity_parse_chown(config, ctx->argv[++ctx->i]) != 0) {
ctx->exit_code = -1;
return true;
+8 -1
View File
@@ -1464,7 +1464,11 @@ static bool send_directory_entry(const Client* client, File* file, const Config*
if (!send_status(client->file_descriptor, STATUS_MKDIR) ||
!send_wire_str(client->file_descriptor, file_wire_path(file)))
return false;
return !config->use_metadata || metadata_send(client->file_descriptor, file->metadata);
if (config->use_metadata && !metadata_send(client->file_descriptor, file->metadata))
return false;
/* Directory xattrs/ACLs (-X/-A) ride the same trailing block as regular files
when the xattr transport was negotiated. */
return !config->use_xattrs || xattr_send(client->file_descriptor, file->xattrs);
}
/* P7 Wave D: transmit every captured source directory's metadata in terminal
@@ -1496,6 +1500,9 @@ static bool send_dir_times(const Client* client, const Config* config, ArrayList
return false;
if (!send_wire_str(fd, file_wire_path(file)) || !metadata_send(fd, file->metadata))
return false;
/* Directory xattrs/ACLs travel with the deferred directory metadata. */
if (config->use_xattrs && !xattr_send(fd, file->xattrs))
return false;
}
index += chunk;
}
+16 -6
View File
@@ -645,7 +645,8 @@ static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
* allocation failure is fatal and reported to the caller. */
static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const char* root_path,
const char* fs_path, bool relative_mode, bool preserve_atimes,
bool preserve_crtimes) {
bool preserve_crtimes, bool preserve_xattrs,
bool preserve_acls) {
if (!dir_entries || !root_path || !fs_path)
return true;
struct stat st;
@@ -672,6 +673,11 @@ static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const
file_destroy(file);
return false;
}
/* Directory xattrs/ACLs (-X/-A): captured here so the deferred
STATUS_DIR_TIMES frame can carry them and the receiver can re-apply them
fd-relative (a regular file's per-file block never covered directories). */
if (preserve_xattrs || preserve_acls)
file->xattrs = xattr_capture_path(fs_path, preserve_acls);
if (relative_mode) {
file->send_path = rel;
rel = NULL;
@@ -771,10 +777,11 @@ static int open_next_directory(DirectoryScanner* scanner) {
return -1;
}
if (scanner->options.capture_dir_times &&
!scanner_capture_dir_time(scanner->options.dir_entries, scanner->options.dir_entries_mutex,
scanner->root_path, scanner->current_path, scanner->relative_mode,
scanner->options.preserve_atimes,
scanner->options.preserve_crtimes)) {
!scanner_capture_dir_time(
scanner->options.dir_entries, scanner->options.dir_entries_mutex, scanner->root_path,
scanner->current_path, scanner->relative_mode, scanner->options.preserve_atimes,
scanner->options.preserve_crtimes, scanner->options.preserve_xattrs,
scanner->options.preserve_acls)) {
closedir(scanner->current_dir);
scanner->current_dir = NULL;
free(scanner->current_path);
@@ -824,6 +831,7 @@ static File* dirs_root_dir_file(DirectoryScanner* scanner) {
return NULL;
}
}
scanner_capture_xattrs(scanner, file);
return file;
}
@@ -937,6 +945,7 @@ static File* dirs_file_for_entry(DirectoryScanner* scanner, const char* entry) {
return NULL;
}
}
scanner_capture_xattrs(scanner, file);
return file;
}
@@ -1760,7 +1769,8 @@ ParallelScanner* parallel_scanner_create_with_options(const char* root_directory
if (options->capture_dir_times &&
!scanner_capture_dir_time(options->dir_entries, options->dir_entries_mutex, root_directory,
root_directory, options->relative && options->file_list != NULL,
options->preserve_atimes, options->preserve_crtimes)) {
options->preserve_atimes, options->preserve_crtimes,
options->preserve_xattrs, options->preserve_acls)) {
array_list_delete(root_files);
array_list_delete(subdirs);
parallel_scanner_destroy(ps);
+9 -7
View File
@@ -197,17 +197,19 @@ void print_usage(void) {
printf(" within the confined receive root. Never elevates\n");
printf(" privileges and never bypasses confinement; ownership\n");
printf(" is still applied only with -o/--owner, -g/--group, or an\n");
printf(" explicit identity flag (--numeric-ids/--chown/--usermap/\n");
printf(" --groupmap/--copy-as)\n");
printf(" explicit identity flag (--chown/--usermap/--groupmap/\n");
printf(" --copy-as); --numeric-ids only changes how ids map\n");
printf(" --no-super Forbid those super-user activities even when the\n");
printf(" receiver is running as root\n");
printf(" --chmod <changes> Modify transferred permissions (rsync syntax)\n");
printf(" --numeric-ids Do not map uid/gid by name: use the source numeric\n");
printf(" ids directly when applying ownership\n");
printf(" --numeric-ids Map uid/gid by id instead of by name (a modifier, not\n");
printf(" an ownership request: combine with -o/-g or a map)\n");
printf(" --usermap=MAP Map usernames when applying ownership: comma-separated\n");
printf(" FROM:TO rules, first match wins. FROM/TO are names\n");
printf(" (resolved on the source machine), * (match any /\n");
printf(" current user), or @N numeric ids. e.g. *:nobody\n");
printf(" FROM:TO rules, first match wins. FROM is a name (from\n");
printf(" the source), an id, an inclusive LOW-HIGH range, *\n");
printf(" (any id), or empty (ids with no name). TO is an id, *\n");
printf(" (current user), or a name resolved on the receiver.\n");
printf(" e.g. 0-99:nobody,*:normal (cannot mix with --chown)\n");
printf(" --groupmap=MAP Map group names when applying ownership (same syntax)\n");
printf(" --chown=USER:GROUP Override the ownership of transferred files. Forms:\n");
printf(" USER:GROUP, USER (owner only), :GROUP (group only); a\n");