feat(parity): rsync 3.4.1 checksum/timeout/temp-dir/connectivity parity (#289 #295 #296)

#289 checksum/compression:
- -c/--checksum now implies the incremental content quick-check (without
  implying -t), so an unchanged file is skipped like rsync.
- --checksum-choice/--cc accepts xxh64/xxhash, xxh3, xxh128, md5 and auto;
  md4/sha1/none and the two-name form are rejected by name.
- --compress-choice/--zc rejects lz4/zlib/zlibx by name (zstd/none/auto kept).
- --checksum-seed=0 is randomized per transfer and sent on the wire.
- --skip-compress uses rsync 3.4.1's default suffix list; slash separators and
  dot-less suffixes are accepted.
- add --no-whole-file.

#295 timeouts/alloc/temp-dir:
- --timeout default 0 (disabled), --contimeout default 60; 0 disables both,
  plus --no-timeout/--no-contimeout.
- --max-alloc=0 means no allocation limit (was rejected).
- --temp-dir accepts any dir, requires it to exist, and falls back to a
  non-atomic copy on EXDEV instead of aborting.

#296 connectivity/daemon:
- -M/--remote-option is rejected for daemon/TCP destinations (SSH-only).
- --trust-sender clarified as receiver-local; server-path tests added.
- --stop-at accepts rsync's full date form (y-m-dTh:m etc.).

Adds unit and integration coverage; no wire-field change, PROTOCOL_VERSION stays
2.22.0.
This commit is contained in:
2026-09-15 22:20:02 +02:00
parent 23552e823d
commit 3eec5a4cc3
25 changed files with 1095 additions and 222 deletions
+71 -22
View File
@@ -126,26 +126,40 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt
return 0;
}
/* Set and validate the compression algorithm selected by the client. */
/* Set and validate the compression algorithm selected by the client. rsync
* 3.4.1 can be built with zstd, none, lz4, zlibx, zlib and auto; FastSync only
* implements zstd (and no compression). "auto" is accepted as the default
* zstd choice; any other rsync choice is rejected by name instead of being
* silently accepted and ignored. */
static int set_compression_choice(Config* config, const char* value) {
if (strcmp(value, "zstd") != 0 && strcmp(value, "none") != 0) {
log_message(LOG_LEVEL_ERROR, "--compress-choice must be zstd or none");
if (strcmp(value, "zstd") != 0 && strcmp(value, "none") != 0 && strcmp(value, "auto") != 0) {
log_message(LOG_LEVEL_ERROR,
"--compress-choice '%s' is not implemented; FastSync supports zstd, none or auto "
"(rsync's lz4/zlib/zlibx are rejected, never silently ignored)",
value);
return -1;
}
if (set_string_option(&config->compress_choice, value, "--compress-choice") != 0)
return -1;
config->use_compression = strcmp(value, "zstd") == 0;
config->use_compression = strcmp(value, "none") != 0;
return 0;
}
/* Validate and store the --checksum-choice/--cc algorithm. Only the algorithms
* the engine genuinely supports are accepted (xxHash64 and md5); anything else
* is a clear error, never a silent no-op. "xxhash" is accepted as rsync's
* spelling of xxHash64. */
* the engine genuinely supports are accepted (xxh64/xxhash, xxh3, xxh128, md5);
* rsync's compiled-in choices that FastSync does not implement (md4, sha1,
* none) and the two-name transfer/pre-transfer syntax are a clear error, never
* a silent no-op. "auto" (rsync's default automatic choice) selects FastSync's
* default algorithm. */
static int set_checksum_choice(Config* config, const char* value) {
if (strcasecmp(value, "auto") == 0)
return 0;
int algo = checksum_algo_from_name(value);
if (algo < 0) {
log_message(LOG_LEVEL_ERROR, "--checksum-choice must be xxh64 (or xxhash) or md5 (got '%s')",
log_message(LOG_LEVEL_ERROR,
"--checksum-choice '%s' is not implemented; FastSync supports xxh64 (or xxhash), "
"xxh3, xxh128, md5 or auto (rsync's md4/sha1/none and the two-name "
"transfer,pre-transfer form are rejected, never silently ignored)",
value);
return -1;
}
@@ -518,10 +532,6 @@ static int parse_size_arg_allow_zero(const char* value, unsigned long long* out,
return 0;
}
static int parse_size_arg(const char* value, unsigned long long* out) {
return parse_size_arg_allow_zero(value, out, false);
}
/* Append a duplicated pattern to a growable pattern array. Returns 0 on success, -1 on error. */
static int config_add_pattern(char*** patterns, int* count, const char* value,
const char* optname) {
@@ -573,7 +583,10 @@ static int parse_skip_compress(Config* config, const char* value) {
if (!list)
return -1;
config->skip_compress_set = true;
for (char* token = strtok(list, ","); token; token = strtok(NULL, ",")) {
/* rsync documents the LIST as slash-separated; accept that along with the
* historical comma-separated spelling. A leading dot is optional (rsync's
* suffixes have none, FastSync historically used them). */
for (char* token = strtok(list, ",/"); token; token = strtok(NULL, ",/")) {
while (*token == ' ' || *token == '\t')
token++;
size_t len = strlen(token);
@@ -741,8 +754,8 @@ static const OptionEntry OPTION_TABLE[] = {
{"--compress-choice", "--zc", OPT_STRING, offsetof(Config, compress_choice)},
{"--compress-level", "--zl", OPT_POS_INT, offsetof(Config, compression_level)},
{"--timeout", NULL, OPT_POS_INT, offsetof(Config, timeout)},
{"--contimeout", NULL, OPT_POS_INT, offsetof(Config, contimeout)},
{"--timeout", NULL, OPT_NONNEG_INT, offsetof(Config, timeout)},
{"--contimeout", NULL, OPT_NONNEG_INT, offsetof(Config, contimeout)},
{"--max-depth", NULL, OPT_NONNEG_INT, offsetof(Config, max_depth)},
{"--address", NULL, OPT_STRING, offsetof(Config, address)},
{"--ipv4", "-4", OPT_FLAG, offsetof(Config, ipv4)},
@@ -781,6 +794,7 @@ static const NegatableOption NEGATABLE_OPTIONS[] = {
{"delete", NULL, offsetof(Config, use_delete)},
{"incremental", NULL, offsetof(Config, use_incremental)},
{"delta", NULL, offsetof(Config, use_delta)},
{"whole-file", "W", offsetof(Config, whole_file)},
{"fuzzy", NULL, offsetof(Config, fuzzy)},
{"save-to-disk", NULL, offsetof(Config, save_to_disk)},
{"progress", NULL, offsetof(Config, show_progress)},
@@ -995,6 +1009,17 @@ static bool cli_handle_pre_negation(CliParseCtx* ctx) {
config->super_mode = SUPER_MODE_OFF;
return true;
}
/* rsync's --no-timeout / --no-contimeout explicit spellings clear the
* corresponding (integer) deadline; handled before the generic --no-* branch
* because the negation table only models boolean fields. */
if (strcmp(arg, "--no-timeout") == 0) {
config->timeout = 0;
return true;
}
if (strcmp(arg, "--no-contimeout") == 0) {
config->contimeout = 0;
return true;
}
if (strncmp(arg, "--no-", strlen("--no-")) == 0) {
if (strcmp(arg, "--no-delta") == 0)
ctx->no_delta = true;
@@ -1051,7 +1076,8 @@ static bool cli_handle_range_time_options(CliParseCtx* ctx) {
}
if (strncmp(arg, "--stop-at=", 10) == 0) {
if (!stop_parse_at_time(arg + 10, time(NULL), &config->stop_at)) {
log_message(LOG_LEVEL_ERROR, "--stop-at must be HH:MM[:SS] or now+N[smhd]");
log_message(LOG_LEVEL_ERROR, "--stop-at must be a date/time such as 2000-12-31T23:59, 12-31, "
"14:00, :59, HH:MM[:SS] or now+N[smhd]");
ctx->exit_code = -1;
return true;
}
@@ -1065,7 +1091,8 @@ static bool cli_handle_range_time_options(CliParseCtx* ctx) {
return true;
}
if (!stop_parse_at_time(ctx->argv[++ctx->i], time(NULL), &config->stop_at)) {
log_message(LOG_LEVEL_ERROR, "--stop-at must be HH:MM[:SS] or now+N[smhd]");
log_message(LOG_LEVEL_ERROR, "--stop-at must be a date/time such as 2000-12-31T23:59, 12-31, "
"14:00, :59, HH:MM[:SS] or now+N[smhd]");
ctx->exit_code = -1;
return true;
}
@@ -1089,8 +1116,11 @@ static bool cli_handle_range_time_options(CliParseCtx* ctx) {
}
value = ctx->argv[++ctx->i];
}
if (parse_size_arg(value, &config->max_alloc) != 0) {
log_message(LOG_LEVEL_ERROR, "--max-alloc must be a positive size (B, K, M, G, T, P, or E)");
/* rsync: --max-alloc=0 means "no alloc limit" (it maps to SIZE_MAX). A
* size with an optional binary suffix is also accepted. */
if (parse_size_arg_allow_zero(value, &config->max_alloc, true) != 0) {
log_message(LOG_LEVEL_ERROR, "--max-alloc must be a size (0 = no limit; B, K, M, G, T, P, E "
"suffixes allowed)");
ctx->exit_code = -1;
}
return true;
@@ -1401,7 +1431,7 @@ static bool cli_handle_transfer_flags(CliParseCtx* ctx) {
const char* arg = ctx->argv[ctx->i];
if (opt_is(arg, "-z", "--compress")) {
config->use_compression =
!config->compress_choice || strcmp(config->compress_choice, "zstd") == 0;
!config->compress_choice || strcmp(config->compress_choice, "none") != 0;
log_info_message(LOG_INFO_MISC, "Enabled Compression");
if (ctx->i + 1 < ctx->argc) {
char* end_ptr;
@@ -2011,7 +2041,16 @@ static bool cli_handle_outbuf_option(CliParseCtx* ctx) {
static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool no_incremental) {
set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING));
if (config->compress_choice)
config->use_compression = strcmp(config->compress_choice, "zstd") == 0;
config->use_compression = strcmp(config->compress_choice, "none") != 0;
/* rsync randomizes the checksum seed for every transfer when the user did not
* supply one (a seed of 0, including an explicit --checksum-seed=0), using
* time(NULL) ^ (getpid() << 6), and transmits it so both ends agree. Mirror
* that: the wire config carries the value, so the receiver uses the exact
* seed this sender hashed with. A non-zero --checksum-seed is honored
* verbatim (deterministic). */
if (config->checksum_seed == 0)
config->checksum_seed = (uint64_t)time(NULL) ^ ((uint64_t)getpid() << 6);
/* --files-from is loaded after every argument is seen so that -0/--from0 may
* appear anywhere on the command line. A missing or unreadable file, and
@@ -2063,6 +2102,16 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
config->use_incremental = true;
}
/* -c/--checksum switches the per-file quick-check from size+mtime to a
* content digest; FastSync expresses that comparison through the incremental
* handshake, so -c implies --incremental. rsync's -c does NOT imply -t (the
* digest alone decides), so the incremental auto-preserve below must not be
* triggered by a checksum-only implication: capture the explicitly requested
* incremental/delta state first. */
bool preserve_implied = config->use_incremental || config->use_delta;
if (config->checksum)
config->use_incremental = true;
/* --incremental/--delta historically auto-enabled the metadata path, which
* applied mode+mtime (README: "--incremental Auto-enables --preserve").
* Restore that behavior by turning on the two attributes unless the user
@@ -2070,7 +2119,7 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
* BEFORE the derived use_metadata bit so the transport frame is still sent
* for the incremental/delta handshake even when both attributes were negated
* via --no-preserve (metadata_explicitly_disabled handles that opt-out). */
if ((config->use_incremental || config->use_delta) && !config->metadata_explicitly_disabled) {
if (preserve_implied && !config->metadata_explicitly_disabled) {
if (!config->preserve_perms_explicit_off)
config->preserve_perms = true;
if (!config->preserve_times_explicit_off)