feat(checksum): --checksum-choice/--cc and --checksum-seed for whole-file digest

Adds real algorithm selection (xxh64 default, plus md5 via OpenSSL EVP) and a
64-bit seed for the per-file whole-file digest used by the --incremental/
--checksum handshake and basis-dir content verification. The seed also feeds
the delta path's per-block xxHash32 strong checksum (low 32 bits) so an
explicit seed deterministically changes those digests too. Sender and receiver
hash identically: the algorithm id and seed cross the config wire frame and the
STATUS_CHECK handshake now carries a length-prefixed, bounded digest instead of
a fixed 64-bit value. Unsupported algorithm names are rejected at parse time
(never a silent no-op). PROTOCOL_VERSION bumped 2.9.0 -> 2.10.0; defaults
(xxh64, seed 0) preserve prior byte-for-byte behavior.
This commit is contained in:
2026-09-07 17:42:47 +02:00
parent 6701c103cb
commit 0afda6b094
12 changed files with 316 additions and 43 deletions
+59 -2
View File
@@ -94,6 +94,47 @@ static int set_compression_choice(Config* config, const char* value) {
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. */
static int set_checksum_choice(Config* config, const char* value) {
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')",
value);
return -1;
}
config->checksum_algo = algo;
return 0;
}
/* parse_ull_arg is defined later in this file; declared here for the seed
parser below. */
static int parse_ull_arg(const char* val, unsigned long long* out, const char* optname);
/* Parse --checksum-seed=NUM as a strict decimal 0..UINT64_MAX. A blank value,
* a sign, or any non-digit (which parse_ull_arg's strtoull would silently
* coerce) is rejected: an explicit seed must be an exact unsigned integer or
* the run fails with a clear error rather than quietly ignoring the value. */
static int set_checksum_seed(Config* config, const char* value) {
if (!value || *value == '\0') {
log_message(LOG_LEVEL_ERROR, "--checksum-seed must be a non-negative integer");
return -1;
}
for (const char* p = value; *p; p++) {
if (*p < '0' || *p > '9') {
log_message(LOG_LEVEL_ERROR, "--checksum-seed must be a non-negative integer");
return -1;
}
}
unsigned long long seed;
if (parse_ull_arg(value, &seed, "--checksum-seed") != 0)
return -1;
config->checksum_seed = seed;
return 0;
}
static int set_compression_threads_option(int* dest, const char* value) {
if (set_positive_int_option(dest, value, "--compress-threads") != 0)
return -1;
@@ -1009,8 +1050,24 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
return -1;
}
log_message(LOG_LEVEL_ERROR, "%s is not supported yet (xxHash64 is used)", argv[i]);
return -1;
if (set_checksum_choice(config, argv[++i]) != 0)
return -1;
} else if (strncmp(argv[i], "--checksum-choice=", 18) == 0) {
if (set_checksum_choice(config, argv[i] + 18) != 0)
return -1;
} else if (strncmp(argv[i], "--cc=", 5) == 0) {
if (set_checksum_choice(config, argv[i] + 5) != 0)
return -1;
} else if (strncmp(argv[i], "--checksum-seed=", 16) == 0) {
if (set_checksum_seed(config, argv[i] + 16) != 0)
return -1;
} else if (opt_is(argv[i], "--checksum-seed", NULL)) {
if (i + 1 >= argc) {
log_message(LOG_LEVEL_ERROR, "missing argument for --checksum-seed");
return -1;
}
if (set_checksum_seed(config, argv[++i]) != 0)
return -1;
} else if (strncmp(argv[i], "--compare-dest=", 15) == 0) {
if (set_basis_dest_option(config, BASIS_DEST_COMPARE, argv[i] + 15, "--compare-dest") != 0)
return -1;
+13 -6
View File
@@ -733,12 +733,18 @@ static int incremental_check(Client* client, File* file, const Config* config,
if (!send_n_data(client->file_descriptor, &mtime_nsec, sizeof(mtime_nsec)))
return -1;
/* With alternate basis directories the receiver must be able to verify the
* content of every candidate basis file, so the sender supplies its xxHash64
* for every file even when --checksum was not requested. */
* content of every candidate basis file, so the sender supplies its whole-file
* digest (computed with the negotiated --checksum-choice algorithm and
* --checksum-seed) for every file even when --checksum was not requested. */
if (config->checksum || config_has_basis(config)) {
uint64_t checksum;
if (!file_checksum(file, &checksum) ||
!send_n_data(client->file_descriptor, &checksum, sizeof(checksum)))
uint8_t digest[CHECKSUM_MAX_DIGEST_LEN];
size_t digest_len = 0;
if (!file_checksum(file, (ChecksumAlgo)config->checksum_algo, config->checksum_seed, digest,
sizeof(digest), &digest_len))
return -1;
uint8_t wire_len = (uint8_t)digest_len;
if (!send_n_data(client->file_descriptor, &wire_len, sizeof(wire_len)) ||
!send_n_data(client->file_descriptor, digest, wire_len))
return -1;
}
Status s;
@@ -774,7 +780,8 @@ static int incremental_check(Client* client, File* file, const Config* config,
}
static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* config) {
Delta* delta = delta_compute(file->data->data, file->data->size, sig, config->delta_block_size);
Delta* delta = delta_compute_seeded(file->data->data, file->data->size, sig,
config->delta_block_size, (uint32_t)config->checksum_seed);
/* The receiver is blocked after sending the signature. Every local
fallback therefore needs the explicit NEXT response before full data. */
if (!delta)
+6 -2
View File
@@ -91,8 +91,12 @@ void print_usage(void) {
printf(" into the destination instead of transferring its data\n");
printf(" --link-dest <dir> Like --copy-dest, but hard-links the unchanged file from DIR\n");
printf(" into the destination (repeatable; earlier DIRs win)\n");
printf(" --checksum-choice, --cc <alg> Checksum algorithm (not supported yet; xxHash64 is "
"used)\n");
printf(" --checksum-choice, --cc <alg> Whole-file checksum algorithm for --incremental/\n");
printf(" --checksum compares (xxh64/xxhash or md5; default xxh64 with\n");
printf(" seed 0). The seed comes from --checksum-seed\n");
printf(" --checksum-seed <num> Seed for the whole-file xxHash64 digest (and the delta\n");
printf(" block strong hash, low 32 bits); md5 ignores the seed. The\n");
printf(" digest algorithm and seed must match on sender and receiver\n");
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
printf(" -W, --whole-file Transfer changed files without delta processing\n");
printf(" -y, --fuzzy Use a similar-named file already in the destination\n");