Merge feat/p4-acl-xattr: -X/-A/--fake-super

# Conflicts:
#	src/shared/config.c
#	src/shared/file.c
#	src/shared/file_types.h
#	tests/integration/test_features.py
#	tests/test_client_cli.c
#	tests/test_config.c
This commit is contained in:
2026-09-08 22:38:15 +02:00
23 changed files with 1297 additions and 52 deletions
+17
View File
@@ -548,6 +548,9 @@ static const OptionEntry OPTION_TABLE[] = {
{"--omit-dir-times", "-O", OPT_FLAG, offsetof(Config, omit_dir_times)},
{"--omit-link-times", "-J", OPT_FLAG, offsetof(Config, omit_link_times)},
{"--open-noatime", NULL, OPT_FLAG, offsetof(Config, open_noatime)},
{"--xattrs", "-X", OPT_FLAG, offsetof(Config, preserve_xattrs)},
{"--acls", "-A", OPT_FLAG, offsetof(Config, preserve_acls)},
{"--fake-super", NULL, OPT_FLAG, offsetof(Config, fake_super)},
};
/* Only boolean options with no required argument are safe to negate. */
@@ -582,6 +585,9 @@ static const NegatableOption NEGATABLE_OPTIONS[] = {
{"preserve", "M", offsetof(Config, use_metadata)},
{"sendfile", "f", offsetof(Config, use_sendfile)},
{"chunk-serialization", "s", offsetof(Config, use_chunk_serialization)},
{"xattrs", "X", offsetof(Config, preserve_xattrs)},
{"acls", "A", offsetof(Config, preserve_acls)},
{"fake-super", NULL, offsetof(Config, fake_super)},
};
static bool opt_is(const char* arg, const char* name, const char* alias) {
@@ -818,6 +824,13 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
if (entry->offset == offsetof(Config, preserve_atimes) ||
entry->offset == offsetof(Config, preserve_crtimes))
config->use_metadata = true;
if (entry->offset == offsetof(Config, preserve_xattrs) ||
entry->offset == offsetof(Config, preserve_acls)) {
config->use_metadata = true;
config->use_xattrs = config->preserve_acls || config->preserve_xattrs;
}
if (entry->offset == offsetof(Config, fake_super))
config->use_metadata = true;
continue;
}
@@ -1265,6 +1278,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for incremental/delta transfer");
config->use_metadata = true;
}
/* Recompute the derived xattr flag from the FINAL preserve flags (after any
* --no-xattrs/--no-acls negation) so the sender's wire gate always matches
* the flags the receiver will recompute from the received config. */
config->use_xattrs = config->preserve_acls || config->preserve_xattrs;
return 0;
}
+15 -5
View File
@@ -20,6 +20,7 @@
#include "transport_ssh.h"
#include "transport_tls.h"
#include "utils.h"
#include "xattr.h"
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
@@ -89,6 +90,8 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
options->use_metadata = config->use_metadata;
options->preserve_atimes = config->preserve_atimes;
options->preserve_crtimes = config->preserve_crtimes;
options->preserve_xattrs = config->preserve_xattrs;
options->preserve_acls = config->preserve_acls;
options->chunk_size = config->chunk_size;
options->exclude_patterns = config->exclude_patterns;
options->exclude_count = config->exclude_count;
@@ -963,6 +966,9 @@ static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* c
if (ok && config->use_metadata)
ok = metadata_send(client->file_descriptor, file->metadata);
if (ok && config->use_xattrs)
ok = xattr_send(client->file_descriptor, file->xattrs);
data_destroy(to_send);
return ok ? 0 : -1;
}
@@ -1004,7 +1010,7 @@ static int send_append(const Client* client, File* file, Config* config,
transfer (byte-identical, never a corrupt prefix+tail blend). */
int rc = file_send_single_calls_with_skip(file, fd, config->use_metadata, compression_level,
false, config->skip_compress_suffixes, skip_count,
config->compression_threads)
config->compression_threads, config->use_xattrs)
? 1
: -1;
return rc;
@@ -1021,6 +1027,9 @@ static int send_append(const Client* client, File* file, Config* config,
if (config->use_metadata && !metadata_send(fd, file->metadata)) {
return -1;
}
if (config->use_xattrs && !xattr_send(fd, file->xattrs)) {
return -1;
}
bool ok;
if (compress) {
/* Compression needs an owned copy of the tail to compress. */
@@ -1058,7 +1067,7 @@ static bool send_file_direct(File* file, int fd, bool use_metadata, int compress
int skip_count = config->skip_compress_set ? config->skip_compress_count : -1;
return file_send_single_calls_with_skip(file, fd, use_metadata, compression_level, true,
config->skip_compress_suffixes, skip_count,
config->compression_threads);
config->compression_threads, config->use_xattrs);
}
/* Transmit one explicit directory entry (--dirs): a STATUS_MKDIR frame whose
@@ -1093,7 +1102,7 @@ static bool send_file_direct_sendfile(File* file, int fd, bool use_metadata, con
int skip_count = config->skip_compress_set ? config->skip_compress_count : -1;
return file_send_sendfile_with_skip(file, fd, use_metadata, 0, true,
config->skip_compress_suffixes, skip_count,
config->compression_threads);
config->compression_threads, config->use_xattrs);
}
// Process one file in a chunk: either via incremental check or direct send.
@@ -1151,7 +1160,7 @@ static int send_single_file(Client* client, File* file, Config* config, bool use
int skip_count = config->skip_compress_set ? config->skip_compress_count : -1;
if (!file_send_sendfile_with_skip(file, client->file_descriptor, config->use_metadata, 0, false,
config->skip_compress_suffixes, skip_count,
config->compression_threads))
config->compression_threads, config->use_xattrs))
return -1;
return 0;
}
@@ -1200,7 +1209,8 @@ static int send_single_file(Client* client, File* file, Config* config, bool use
int skip_count = config->skip_compress_set ? config->skip_compress_count : -1;
if (!file_send_single_calls_with_skip(file, client->file_descriptor, config->use_metadata,
compression_level, false, config->skip_compress_suffixes,
skip_count, config->compression_threads))
skip_count, config->compression_threads,
config->use_xattrs))
return -1;
return 0;
}
+8
View File
@@ -79,6 +79,14 @@ bool validate_config(const Config* config) {
"--hard-links/-H cannot be combined with -s (chunk serialization)");
return false;
}
/* -X/-A ride the per-file metadata frame; the buffer-based chunk-serialization
wire format does not carry the xattr block, so the pair is rejected up front
(mirroring -H + -s) rather than silently dropping attributes. */
if ((config->preserve_xattrs || config->preserve_acls) && config->use_chunk_serialization) {
log_message(LOG_LEVEL_ERROR,
"--xattrs/-X and --acls/-A cannot be combined with -s (chunk serialization)");
return false;
}
if (config->preserve_hard_links && (config->append || config->append_verify)) {
log_message(LOG_LEVEL_ERROR,
"--hard-links/-H cannot be combined with --append/--append-verify");
+17
View File
@@ -15,6 +15,8 @@
#include <unistd.h>
#include <limits.h>
#include "xattr.h"
typedef struct {
char* path;
int depth;
@@ -172,6 +174,14 @@ static bool entry_passes_selection(const FileListSet* file_list, const FilterRul
return true;
}
/* Best-effort capture of the file's whitelisted xattrs (-X/-A). A failure to
* read xattrs is non-fatal: the file is transferred without them. */
static void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file) {
if (!scanner || !file || !(scanner->preserve_xattrs || scanner->preserve_acls))
return;
file->xattrs = xattr_capture_path(file->path);
}
/* Apply --hard-links (-H) detection to one regular File. On a sibling (a
* later member of an already-seen source inode) the File keeps the group id
* and the first member's wire path but carries NO data payload (size 0); the
@@ -432,6 +442,8 @@ DirectoryScanner* directory_scanner_create_with_options(const char* root_directo
scanner->use_metadata = options->use_metadata;
scanner->preserve_atimes = options->preserve_atimes;
scanner->preserve_crtimes = options->preserve_crtimes;
scanner->preserve_xattrs = options->preserve_xattrs;
scanner->preserve_acls = options->preserve_acls;
scanner->chunk_size = options->chunk_size > 0 ? options->chunk_size : DESIRED_CHUNK_SIZE;
scanner->exclude_patterns = options->exclude_patterns;
scanner->exclude_count = options->exclude_count;
@@ -1036,6 +1048,8 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
scanner->failed = true;
break;
}
if (!(file->link_group != 0 && !file->link_first))
scanner_capture_xattrs(scanner, file);
if (!array_list_add(chunk_data, file)) {
free(rel_copy);
file_destroy(file);
@@ -1379,6 +1393,9 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
ps->failed = true;
return;
}
if ((options->preserve_xattrs || options->preserve_acls) &&
!(file->link_group != 0 && !file->link_first))
file->xattrs = xattr_capture_path(file->path);
if (!array_list_add(root_files, file)) {
free(rel);
file_destroy(file);
+6
View File
@@ -19,6 +19,10 @@ typedef struct {
* capture the source access / birth time into each entry's FileMetadata. */
bool preserve_atimes;
bool preserve_crtimes;
/* Phase 4 xattrs: when preserve_xattrs || preserve_acls is set the scanner
* captures each regular file's whitelisted xattr set onto the File. */
bool preserve_xattrs;
bool preserve_acls;
unsigned long long chunk_size;
char** exclude_patterns;
int exclude_count;
@@ -101,6 +105,8 @@ typedef struct {
bool use_metadata;
bool preserve_atimes;
bool preserve_crtimes;
bool preserve_xattrs;
bool preserve_acls;
unsigned long long chunk_size;
char** exclude_patterns;
int exclude_count;
+10
View File
@@ -128,6 +128,16 @@ void print_usage(void) {
printf(" none suppresses info even with --verbose\n");
printf(" -M, --preserve Preserve file metadata\n");
printf(" -E, --executability Preserve executable permission bits\n");
printf(" -X, --xattrs Preserve user extended attributes (user.* only;\n");
printf(" privileged security.*/trusted.* namespaces are\n");
printf(" never captured or applied)\n");
printf(" -A, --acls Preserve POSIX ACLs (the system.posix_acl_* xattrs;\n");
printf(" setting an ACL the receiver is not permitted to\n");
printf(" set is warned and skipped, never fatal)\n");
printf(" --fake-super Store the source uid/gid/mode/mtime in a reserved\n");
printf(" user.fastsync.stat xattr on each written file instead\n");
printf(" of applying ownership (for a later privileged restore);\n");
printf(" partial: full rsync fake-super replay is out of scope\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");