Pure structural split of src/shared/file_receive.c into focused translation units behind the unchanged file_receive.h facade: - file_save.c : save-to-disk, special nodes, --delay-updates staging - incremental_check.c : xattr/delta/basis/fuzzy receive + check state machine - delete_commit.c : manifest receive + delete budget walkers - file_receive.c : wire receive dispatch + deferred dir metadata The shared receive_file_xattrs helper and MAX_FILE_DATA_SIZE are declared in incremental_check.h. file_save_to_disk_full_ex is decomposed into static helpers (validation, special dispatch, dir/symlink creation, path resolution, pre-write policies, data install) routed through one cleanup epilogue. No behavior change.
1677 lines
63 KiB
C
1677 lines
63 KiB
C
#include <errno.h>
|
|
#include <ctype.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <libgen.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/sysmacros.h>
|
|
#include <unistd.h>
|
|
|
|
#include "array_list.h"
|
|
#include "charset.h"
|
|
#include "chmod.h"
|
|
#include "chunk.h"
|
|
#include "compression.h"
|
|
#include "config.h"
|
|
#include "data.h"
|
|
#include "delay_updates.h"
|
|
#include "delta.h"
|
|
#include "file.h"
|
|
#include "format.h"
|
|
#include "identity.h"
|
|
#include "incremental_check.h"
|
|
#include "log.h"
|
|
#include "metadata.h"
|
|
#include "protocol.h"
|
|
#include "utils.h"
|
|
#include "xattr.h"
|
|
|
|
/* Receive a file's xattr block (when the config enables xattr transport) and
|
|
* attach it to `file`. Returns false on a malformed/oversized frame. */
|
|
bool receive_file_xattrs(File* file, int fd, const Config* config) {
|
|
if (!config->use_xattrs)
|
|
return true;
|
|
int xok = 0;
|
|
FileXattrList* list = xattr_receive(fd, &xok, config->preserve_acls);
|
|
if (!xok) {
|
|
xattr_list_free(list);
|
|
return false;
|
|
}
|
|
file->xattrs = list;
|
|
return true;
|
|
}
|
|
|
|
static File* receive_delta_file(int fd, const Config* config, const char* check_path,
|
|
void* old_data, unsigned long long old_size, bool* failed) {
|
|
if (!old_data) {
|
|
free(old_data); /* defensive: old_data is always non-NULL today */
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
DeltaSignature* sig = delta_signature_create_seeded(old_data, old_size, config->delta_block_size,
|
|
(uint32_t)config->checksum_seed);
|
|
if (!sig) {
|
|
free(old_data);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
Data* sig_data = delta_signature_serialize(sig);
|
|
if (!sig_data) {
|
|
delta_signature_destroy(sig);
|
|
free(old_data);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
bool sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data);
|
|
data_destroy(sig_data);
|
|
|
|
if (!sig_sent) {
|
|
delta_signature_destroy(sig);
|
|
free(old_data);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
Status resp;
|
|
if (!receive_status(fd, &resp)) {
|
|
delta_signature_destroy(sig);
|
|
free(old_data);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
if (resp == STATUS_DELTA_DATA) {
|
|
Data* delta_data = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
if (!delta_data) {
|
|
delta_signature_destroy(sig);
|
|
free(old_data);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
Data* raw_delta = delta_data;
|
|
if (config->use_compression &&
|
|
!compression_should_skip_with_suffixes(
|
|
check_path, config->skip_compress_suffixes,
|
|
config->skip_compress_set ? config->skip_compress_count : -1)) {
|
|
ProtocolSession* owner = delta_data->owner;
|
|
raw_delta = data_decompress_limited(delta_data, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
data_destroy(delta_data);
|
|
if (!raw_delta) {
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
/* Charge the decompressed delta to the connection budget (the paired
|
|
wire buffer's charge was just released). */
|
|
if (!data_charge_session(raw_delta, owner, raw_delta->size)) {
|
|
data_destroy(raw_delta);
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
Delta* delta = delta_deserialize(raw_delta);
|
|
data_destroy(raw_delta);
|
|
if (!delta) {
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
uint64_t new_size = delta->new_file_size;
|
|
if (new_size > MAX_RECEIVE_WHOLE_FILE_SIZE || new_size > SIZE_MAX) {
|
|
delta_destroy(delta);
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
send_status(fd, STATUS_ERROR);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
/* Wire-stats tally: bytes taken straight from the basis file (matched
|
|
delta blocks) and bytes shipped literally (protocol 2.28.0). Computed
|
|
before the delta is destroyed. */
|
|
unsigned long long matched = 0;
|
|
unsigned long long literal = 0;
|
|
for (uint32_t k = 0; k < delta->instruction_count; k++) {
|
|
if (delta->instructions[k].type == DELTA_INSTR_BLOCK_MATCH)
|
|
matched += delta->instructions[k].match.length;
|
|
else if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
|
|
literal += delta->instructions[k].literal.length;
|
|
}
|
|
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size);
|
|
delta_destroy(delta);
|
|
|
|
if (!new_data) {
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
File* file = file_create(check_path);
|
|
if (!file) {
|
|
free(new_data);
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
file->matched_bytes = matched;
|
|
file->literal_bytes = literal;
|
|
|
|
if (config->use_metadata) {
|
|
int meta_ok = 1;
|
|
file->metadata = metadata_receive(fd, &meta_ok);
|
|
if (!meta_ok) {
|
|
file_destroy(file);
|
|
free(new_data);
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
}
|
|
if (!receive_file_xattrs(file, fd, config)) {
|
|
file_destroy(file);
|
|
free(new_data);
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
Data* replacement = data_create(new_data, (size_t)new_size);
|
|
if (replacement == NULL) {
|
|
file_destroy(file);
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
send_status(fd, STATUS_ERROR);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
data_destroy(file->data);
|
|
file->data = replacement;
|
|
|
|
free(old_data);
|
|
delta_signature_destroy(sig);
|
|
return file;
|
|
}
|
|
|
|
if (resp == STATUS_NEXT) {
|
|
delta_signature_destroy(sig);
|
|
free(old_data);
|
|
|
|
File* file = file_create(check_path);
|
|
if (!file) {
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
if (config->use_metadata) {
|
|
int meta_ok = 1;
|
|
file->metadata = metadata_receive(fd, &meta_ok);
|
|
if (!meta_ok) {
|
|
file_destroy(file);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
}
|
|
if (!receive_file_xattrs(file, fd, config)) {
|
|
file_destroy(file);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
Data* file_data = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
if (file_data == NULL) {
|
|
file_destroy(file);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
if (config->use_compression &&
|
|
!compression_should_skip_with_suffixes(
|
|
file->path, config->skip_compress_suffixes,
|
|
config->skip_compress_set ? config->skip_compress_count : -1)) {
|
|
Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
ProtocolSession* owner = file_data->owner;
|
|
data_destroy(file_data);
|
|
if (uncompressed == NULL) {
|
|
file_destroy(file);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
if (!data_charge_session(uncompressed, owner, uncompressed->size)) {
|
|
data_destroy(uncompressed);
|
|
file_destroy(file);
|
|
send_status(fd, STATUS_ERROR);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
if (uncompressed->size > MAX_FILE_DATA_SIZE) {
|
|
data_destroy(uncompressed);
|
|
file_destroy(file);
|
|
send_status(fd, STATUS_ERROR);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
file_data = uncompressed;
|
|
}
|
|
|
|
data_destroy(file->data);
|
|
file->data = file_data;
|
|
return file;
|
|
}
|
|
|
|
delta_signature_destroy(sig);
|
|
free(old_data);
|
|
send_status(fd, STATUS_ERROR);
|
|
*failed = true;
|
|
return NULL;
|
|
}
|
|
|
|
/* ---- Alternate basis directories (--compare-dest / --copy-dest / --link-dest) ----
|
|
* The receiver consults the ordered basis-dir list only when the destination
|
|
* entry is NOT already up to date. By default an "exact match" is rsync's
|
|
* metadata quick-check: an equal size and an equal mtime (unless --size-only).
|
|
* The FastSync-only --verify-basis additionally requires an equal whole-file
|
|
* content digest, so a hard link / local copy is only then made from
|
|
* byte-verified content. */
|
|
|
|
typedef struct BasisMatch {
|
|
bool hit;
|
|
BasisDestType type;
|
|
char* basis_path; /* owned absolute path of the matched basis file */
|
|
struct stat st; /* fstat() of the matched basis file */
|
|
} BasisMatch;
|
|
|
|
static void basis_match_free(BasisMatch* match) {
|
|
if (!match)
|
|
return;
|
|
free(match->basis_path);
|
|
match->basis_path = NULL;
|
|
match->hit = false;
|
|
match->type = BASIS_DEST_NONE;
|
|
}
|
|
|
|
/* Open `path` (via the secure, root-confined primitives) and require it to be
|
|
a regular file of exactly `expected_size` bytes. Returns an open read-only
|
|
descriptor and its fstat on success. */
|
|
static bool basis_open_regular(const char* path, unsigned long long expected_size, int* out_fd,
|
|
struct stat* out_st) {
|
|
char* leaf = NULL;
|
|
int parent_fd = file_open_secure_parent(path, &leaf, false);
|
|
if (parent_fd < 0)
|
|
return false;
|
|
/* O_NONBLOCK: a client-planted FIFO must not block the receiver's openat()
|
|
forever; the fstat()/S_ISREG gate below rejects it immediately. */
|
|
int fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
|
|
free(leaf);
|
|
close(parent_fd);
|
|
if (fd < 0)
|
|
return false;
|
|
struct stat st;
|
|
if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) ||
|
|
(unsigned long long)st.st_size != expected_size) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
*out_fd = fd;
|
|
*out_st = st;
|
|
return true;
|
|
}
|
|
|
|
/* --ignore-times forces every file to be updated, so no basis hit is ever
|
|
declared (matching rsync, where -I prevents link-dest from linking). */
|
|
bool file_basis_quick_match(const Config* config, const struct stat* st, time_t check_mtime,
|
|
long check_mtime_nsec) {
|
|
if (config->size_only)
|
|
return true;
|
|
long mtime_nsec = 0;
|
|
#ifdef __linux__
|
|
mtime_nsec = st->st_mtim.tv_nsec;
|
|
#endif
|
|
return metadata_mtime_matches(st->st_mtime, mtime_nsec, check_mtime, check_mtime_nsec,
|
|
config->modify_window);
|
|
}
|
|
|
|
/* True when a basis hit must be confirmed by a whole-file content digest
|
|
(--verify-basis). False is the rsync-parity default: the metadata
|
|
quick-check alone decides a hit. */
|
|
bool file_basis_content_required(const Config* config) {
|
|
return config != NULL && config->verify_basis;
|
|
}
|
|
|
|
/* Probe one candidate basis file: open it (confined, O_NOFOLLOW) and apply
|
|
rsync's metadata quick-check; under --verify-basis also hash its bytes and
|
|
require the sender's digest. On a hit record `candidate` in `out` and return
|
|
true. The caller retains ownership of `candidate`. */
|
|
static bool basis_match_probe(const Config* config, const char* candidate,
|
|
unsigned long long check_size, time_t check_mtime,
|
|
long check_mtime_nsec, const uint8_t* check_digest,
|
|
size_t check_digest_len, BasisDestType type, BasisMatch* out) {
|
|
int fd;
|
|
struct stat st;
|
|
if (!basis_open_regular(candidate, check_size, &fd, &st))
|
|
return false;
|
|
bool hit = false;
|
|
if (file_basis_quick_match(config, &st, check_mtime, check_mtime_nsec)) {
|
|
hit = true;
|
|
if (file_basis_content_required(config)) {
|
|
uint8_t basis_digest[CHECKSUM_MAX_DIGEST_LEN];
|
|
size_t basis_len = 0;
|
|
bool hashed = checksum_digest_fd((ChecksumAlgo)config->checksum_algo, config->checksum_seed,
|
|
fd, basis_digest, sizeof(basis_digest), &basis_len);
|
|
hit = hashed && basis_len == check_digest_len && check_digest_len > 0 &&
|
|
memcmp(basis_digest, check_digest, check_digest_len) == 0;
|
|
}
|
|
}
|
|
close(fd);
|
|
if (!hit)
|
|
return false;
|
|
char* owned = str_dup(candidate);
|
|
if (!owned)
|
|
return false;
|
|
out->hit = true;
|
|
out->type = type;
|
|
out->basis_path = owned;
|
|
out->st = st;
|
|
return true;
|
|
}
|
|
|
|
/* Search the basis-dir list in command-line order and return the first match.
|
|
By default (no --verify-basis) rsync's metadata quick-check is sufficient:
|
|
basis_open_regular has already required an equal size, and
|
|
file_basis_quick_match applies rsync's mtime (or --size-only) rule.
|
|
--verify-basis additionally requires the basis bytes' whole-file digest to
|
|
equal the sender's, restoring FastSync's historical content equality; that
|
|
digest is computed by streaming the open basis descriptor, so an arbitrarily
|
|
large basis is verified without buffering it. A copy/link install re-reads
|
|
the basis from its path in bounded buffers, so no content buffer is kept.
|
|
|
|
`hash_content` gates content READS under --verify-basis: a server-contacting
|
|
--dry-run passes false because hashing a basis against a client-supplied
|
|
digest would be a 1-bit content oracle. Without --verify-basis a dry-run can
|
|
still confirm the metadata-only hit without reading any basis bytes, matching
|
|
rsync's read-only quick-check.
|
|
|
|
Path resolution (rsync 3.4.1 parity): rsync resolves a relative
|
|
--compare-dest/--copy-dest/--link-dest DIR against the destination directory
|
|
(the receiver's cwd) and appends the file's TRANSFER-RELATIVE name, e.g.
|
|
`--compare-dest=basis` with `rsync src/ dst/` probes `dst/basis/<name-inside-src>`.
|
|
FastSync's receive root IS the destination directory, but its default transfer
|
|
mirrors the absolute source path below that root, so check_path carries the
|
|
source-root scaffolding rsync would not append. Recover rsync's spelling with
|
|
utils_strip_transfer_root for a relative DIR; under -R/--files-from the wire
|
|
path is already transfer-relative, so it is used as-is. A relative DIR also
|
|
probes the historical mirror-appended spelling as a fallback, so existing
|
|
FastSync-laid-out snapshot trees keep resolving. An absolute DIR is used
|
|
verbatim and keeps appending the destination-relative check_path (FastSync's
|
|
mirrored layout). Every candidate stays confined to the authorized root by
|
|
file_open_secure_parent. */
|
|
static bool basis_match_find(const Config* config, const char* check_path,
|
|
unsigned long long check_size, time_t check_mtime,
|
|
long check_mtime_nsec, const uint8_t* check_digest,
|
|
size_t check_digest_len, bool hash_content, BasisMatch* out) {
|
|
memset(out, 0, sizeof(*out));
|
|
if (!config || !config_has_basis(config) || config->ignore_times)
|
|
return false;
|
|
/* --verify-basis needs the basis content; a content-blind (dry-run) pass can
|
|
never confirm it and must not read the file, so decline without touching
|
|
the basis bytes. */
|
|
if (file_basis_content_required(config) && !hash_content)
|
|
return false;
|
|
const char* transfer_rel = check_path;
|
|
if (!config->relative && config->files_from_set == NULL)
|
|
transfer_rel = utils_strip_transfer_root(check_path, config->send_directory);
|
|
for (int i = 0; i < config->basis_count; i++) {
|
|
const BasisDest* entry = &config->basis_dirs[i];
|
|
/* An absolute basis path is used verbatim (rsync semantics); a relative one
|
|
is resolved below the receive root. Both remain subject to the receiver's
|
|
authorized-root confinement inside file_open_secure_parent. */
|
|
bool absolute = entry->path[0] == '/';
|
|
char* basis_dir =
|
|
absolute ? str_dup(entry->path) : path_cat(config->receive_root_directory, entry->path);
|
|
if (!basis_dir)
|
|
continue;
|
|
const char* names[2];
|
|
int name_count = 0;
|
|
if (absolute)
|
|
names[name_count++] = check_path;
|
|
else
|
|
names[name_count++] = transfer_rel;
|
|
if (!absolute && strcmp(transfer_rel, check_path) != 0)
|
|
names[name_count++] = check_path; /* historical mirror-appended spelling */
|
|
bool found = false;
|
|
for (int n = 0; n < name_count && !found; n++) {
|
|
char* candidate = path_cat(basis_dir, names[n]);
|
|
if (!candidate)
|
|
continue;
|
|
found = basis_match_probe(config, candidate, check_size, check_mtime, check_mtime_nsec,
|
|
check_digest, check_digest_len, entry->type, out);
|
|
free(candidate);
|
|
}
|
|
free(basis_dir);
|
|
if (found)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* -y/--fuzzy similar-file delta basis.
|
|
*
|
|
* When a file must be transferred and the destination holds no usable content
|
|
* at the exact path (the destination file is absent, or is outside the delta
|
|
* engine's size bounds), --fuzzy lets the receiver reuse an EXISTING regular
|
|
* file in the SAME destination directory as the delta basis, so the sender
|
|
* transmits only the differences instead of the whole file. This is the
|
|
* rsync "find a similar file to use as a basis for a transfer" case (e.g. a
|
|
* file recreated under a new name whose old-named sibling is still present).
|
|
*
|
|
* The delta handshake is unchanged and receiver-driven, so the sender never
|
|
* learns the basis was a different file and needs no new protocol. Byte
|
|
* exactness never depends on which bytes the basis holds: the delta protocol
|
|
* only references basis blocks whose Adler-32 + xxHash32 checksums match the
|
|
* source, delta_apply validates every reference against the basis size, and a
|
|
* basis that shares nothing simply makes the sender reply STATUS_NEXT (full
|
|
* transfer). A fuzzy basis can therefore waste bandwidth but never corrupt a
|
|
* file.
|
|
*
|
|
* Similarity heuristic (rsync 3.4.1 parity, util1.c fuzzy_distance /
|
|
* find_filename_suffix + generator.c find_fuzzy):
|
|
* * candidates are the target's sibling entries in its destination
|
|
* directory, opened through the confined root (file_open_secure_parent +
|
|
* openat O_NOFOLLOW, fstatat AT_SYMLINK_NOFOLLOW) -- symlinks are never
|
|
* followed and nothing outside the destination root is ever read;
|
|
* * dotfiles, directories, the target's own name, and the .fastsync-stage /
|
|
* temp scratch names are never candidates;
|
|
* * size gate = rsync's, NOT the ordinary delta engine's bounds: any
|
|
* non-empty regular sibling up to the receiver's whole-file buffer cap is
|
|
* eligible, regardless of the 16 KiB delta minimum or the 10x delta size
|
|
* ratio (rsync's find_fuzzy has no delta-size gate at all). The delta
|
|
* engine consumes the fuzzy basis through the same signature handshake
|
|
* whether or not it is inside delta_should_attempt's window;
|
|
* * first pass = an exact size+mtime match wins regardless of name (rsync's
|
|
* "fuzzy size/modtime match");
|
|
* * otherwise the winner minimizes rsync's weighted Levenshtein distance
|
|
* (substitution ± byte difference, insertion UNIT+byte, 16.16 fixed point)
|
|
* plus ten times the suffix distance, accepted only when <= 25*UNIT; the
|
|
* tie-break (smallest size gap, then lexical name) keeps the result
|
|
* deterministic across filesystem readdir order (rsync leaves equal
|
|
* distances to its file-list order).
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/* A directory scan is linear in the number of entries; the fuzzy search stops
|
|
* after this many so a pathological huge directory cannot stall a transfer.
|
|
* The cap bounds the readdir() ITERATIONS, not the per-entry work: every
|
|
* entry that survives the (cheap) size and pre-name gates still runs an
|
|
* edit-distance DP, so the per-entry DP cost is separately bounded below by
|
|
* pre-pruning on the name length gap and the absent-character bound, and by
|
|
* trimming the common prefix/suffix before the DP runs on the middles only. */
|
|
#define FUZZY_MAX_DIRECTORY_SCAN 4096
|
|
/* Names longer than this never take part in fuzzy matching: the edit-distance
|
|
* DP below is O(len^2), so over-long names are bounded out of the search. */
|
|
#define FUZZY_NAME_LIMIT 192
|
|
|
|
typedef struct {
|
|
char name[FUZZY_NAME_LIMIT + 1];
|
|
unsigned long long size;
|
|
uint32_t distance;
|
|
unsigned long long size_gap;
|
|
} FuzzyCandidate;
|
|
|
|
/* rsync's fuzzy distance is a weighted Levenshtein variant in 16.16 fixed point
|
|
* (util1.c fuzzy_distance): a substitution costs UNIT +/- the byte difference
|
|
* and an insertion costs UNIT + the inserted byte, so similar names score low.
|
|
* The search keeps only distances <= 25*UNIT. Ported verbatim for parity. */
|
|
#define FUZZY_DIST_UNIT (1u << 16)
|
|
#define FUZZY_DIST_REJECT (0xFFFFu * FUZZY_DIST_UNIT + 1)
|
|
#define FUZZY_DIST_LIMIT (25u * FUZZY_DIST_UNIT)
|
|
|
|
static uint32_t fuzzy_distance(const char* s1, unsigned len1, const char* s2, unsigned len2,
|
|
uint32_t upperlimit, uint32_t* scratch) {
|
|
if ((len1 > len2 ? len1 - len2 : len2 - len1) * FUZZY_DIST_UNIT > upperlimit)
|
|
return FUZZY_DIST_REJECT;
|
|
if (!len1 || !len2) {
|
|
if (!len1) {
|
|
s1 = s2;
|
|
len1 = len2;
|
|
}
|
|
uint32_t cost = 0;
|
|
for (unsigned i = 0; i < len1; i++)
|
|
cost += (uint8_t)s1[i];
|
|
return (uint32_t)len1 * FUZZY_DIST_UNIT + cost;
|
|
}
|
|
uint32_t* a = scratch;
|
|
for (unsigned i2 = 0; i2 < len2; i2++)
|
|
a[i2] = (i2 + 1) * FUZZY_DIST_UNIT;
|
|
for (unsigned i1 = 0; i1 < len1; i1++) {
|
|
uint32_t diag = i1 * FUZZY_DIST_UNIT;
|
|
uint32_t above = (i1 + 1) * FUZZY_DIST_UNIT;
|
|
for (unsigned i2 = 0; i2 < len2; i2++) {
|
|
uint32_t left = a[i2];
|
|
int32_t cost = (int32_t)(uint8_t)s1[i1] - (int32_t)(uint8_t)s2[i2];
|
|
if (cost != 0)
|
|
cost = cost < 0 ? (int32_t)(FUZZY_DIST_UNIT - (uint32_t)(-cost))
|
|
: (int32_t)(FUZZY_DIST_UNIT + (uint32_t)cost);
|
|
uint32_t diag_inc = diag + (uint32_t)cost;
|
|
uint32_t left_inc = left + FUZZY_DIST_UNIT + (uint8_t)s1[i1];
|
|
uint32_t above_inc = above + FUZZY_DIST_UNIT + (uint8_t)s2[i2];
|
|
a[i2] = above = left < above ? (left_inc < diag_inc ? left_inc : diag_inc)
|
|
: (above_inc < diag_inc ? above_inc : diag_inc);
|
|
diag = left;
|
|
}
|
|
}
|
|
return a[len2 - 1];
|
|
}
|
|
|
|
/* rsync's find_filename_suffix (util1.c): return the last significant filename
|
|
* suffix (its dot included). Leading dots are not a suffix; a trailing "~" is
|
|
* ignored; .bak/.old/.orig and a "~/<num>" backup marker are skipped. */
|
|
static const char* fuzzy_find_suffix(const char* fn, int fn_len, int* len_ptr) {
|
|
const char* suf;
|
|
const char* s;
|
|
bool had_tilde;
|
|
|
|
while (fn_len && *fn == '.') {
|
|
fn++;
|
|
fn_len--;
|
|
}
|
|
if (fn_len > 1 && fn[fn_len - 1] == '~') {
|
|
fn_len--;
|
|
had_tilde = true;
|
|
} else {
|
|
had_tilde = false;
|
|
}
|
|
suf = "";
|
|
*len_ptr = 0;
|
|
for (s = fn + fn_len; fn_len > 1;) {
|
|
int s_len;
|
|
while (--s != fn && *s != '.') {
|
|
}
|
|
if (s == fn)
|
|
break;
|
|
s_len = fn_len - (int)(s - fn);
|
|
fn_len = (int)(s - fn);
|
|
if (s_len == 4) {
|
|
if (strcmp(s + 1, "bak") == 0 || strcmp(s + 1, "old") == 0)
|
|
continue;
|
|
} else if (s_len == 5) {
|
|
if (strcmp(s + 1, "orig") == 0)
|
|
continue;
|
|
} else if (s_len > 2 && had_tilde && s[1] == '~' && isdigit((unsigned char)s[2])) {
|
|
continue;
|
|
}
|
|
*len_ptr = s_len;
|
|
suf = s;
|
|
if (s_len == 1)
|
|
break;
|
|
for (s++, s_len--; s_len > 0; s++, s_len--) {
|
|
if (!isdigit((unsigned char)*s))
|
|
return suf;
|
|
}
|
|
s = suf;
|
|
}
|
|
return suf;
|
|
}
|
|
|
|
/* Deterministic ordering of two fuzzy candidates with equal rsync distance:
|
|
* smallest size gap, then the lexical basename (rsync itself takes the last
|
|
* equal-distance candidate in file-list order). */
|
|
static bool fuzzy_candidate_better(const FuzzyCandidate* cand, const FuzzyCandidate* best) {
|
|
if (!best->name[0])
|
|
return true;
|
|
if (cand->distance != best->distance)
|
|
return cand->distance < best->distance;
|
|
if (cand->size_gap != best->size_gap)
|
|
return cand->size_gap < best->size_gap;
|
|
return strcmp(cand->name, best->name) < 0;
|
|
}
|
|
|
|
/* Search the destination directory that will contain `check_path` for a
|
|
* similar regular file usable as a --fuzzy delta basis and return its full
|
|
* content in a malloc'd (protocol_alloc) buffer. Returns NULL (with *out_size
|
|
* = 0) when no candidate qualifies, which means the caller performs the normal
|
|
* whole-file transfer. */
|
|
static void* fuzzy_basis_find_and_load(const Config* config, const char* check_path,
|
|
unsigned long long check_size, time_t check_mtime,
|
|
long check_mtime_nsec, unsigned long long* out_size) {
|
|
*out_size = 0;
|
|
if (!config || !config->receive_root_directory || !config->fuzzy || !config->use_delta ||
|
|
!check_path || check_size > MAX_RECEIVE_WHOLE_FILE_SIZE)
|
|
return NULL;
|
|
|
|
char* full_path = path_cat(config->receive_root_directory, check_path);
|
|
if (!full_path)
|
|
return NULL;
|
|
char* leaf = NULL;
|
|
int dir_fd = file_open_secure_parent(full_path, &leaf, false);
|
|
if (dir_fd < 0 || !leaf) {
|
|
free(leaf);
|
|
free(full_path);
|
|
return NULL;
|
|
}
|
|
size_t target_len = strlen(leaf);
|
|
/* A target basename longer than FUZZY_NAME_LIMIT can never pass the name gate
|
|
(every candidate name is bounded by the same limit), so skip the scan. */
|
|
if (target_len > FUZZY_NAME_LIMIT) {
|
|
close(dir_fd);
|
|
free(leaf);
|
|
free(full_path);
|
|
return NULL;
|
|
}
|
|
|
|
int scanfd = dup(dir_fd);
|
|
if (scanfd < 0) {
|
|
close(dir_fd);
|
|
free(leaf);
|
|
free(full_path);
|
|
return NULL;
|
|
}
|
|
DIR* dir = fdopendir(scanfd);
|
|
if (!dir) {
|
|
close(scanfd);
|
|
close(dir_fd);
|
|
free(leaf);
|
|
free(full_path);
|
|
return NULL;
|
|
}
|
|
|
|
/* The weighted-distance scratch row is allocated once per scan (not once per
|
|
candidate). */
|
|
uint32_t* dist_scratch = malloc((FUZZY_NAME_LIMIT + 1) * sizeof(uint32_t));
|
|
if (!dist_scratch) {
|
|
closedir(dir);
|
|
close(dir_fd);
|
|
free(leaf);
|
|
free(full_path);
|
|
return NULL;
|
|
}
|
|
int fname_suf_len = 0;
|
|
const char* fname_suf = fuzzy_find_suffix(leaf, (int)target_len, &fname_suf_len);
|
|
|
|
FuzzyCandidate best;
|
|
memset(&best, 0, sizeof(best));
|
|
uint32_t lowest_dist = FUZZY_DIST_LIMIT;
|
|
/* rsync's fuzzy search runs an exact size+mtime pass before the name-distance
|
|
pass; such a candidate is almost certainly the same content and wins
|
|
regardless of how dissimilar its name is. The first one (directory order,
|
|
deterministic) is kept. */
|
|
FuzzyCandidate exact;
|
|
memset(&exact, 0, sizeof(exact));
|
|
const struct dirent* entry;
|
|
size_t scanned = 0;
|
|
/* readdir() yields entries in filesystem-dependent order, so the SET of
|
|
candidates seen is order-dependent; the winner is still deterministic
|
|
because every candidate is compared with the total ordering in
|
|
fuzzy_candidate_better (acceptable for a heuristic). */
|
|
while (scanned < FUZZY_MAX_DIRECTORY_SCAN && (entry = readdir(dir)) != NULL) {
|
|
scanned++;
|
|
const char* name = entry->d_name;
|
|
size_t name_len = strlen(name);
|
|
if (name[0] == '.' || name_len == 0 || name_len > FUZZY_NAME_LIMIT || strcmp(name, leaf) == 0)
|
|
continue;
|
|
struct stat st;
|
|
if (fstatat(dir_fd, name, &st, AT_SYMLINK_NOFOLLOW) != 0 || !S_ISREG(st.st_mode))
|
|
continue;
|
|
unsigned long long cand_size = (unsigned long long)st.st_size;
|
|
if (cand_size == 0 || cand_size > MAX_RECEIVE_WHOLE_FILE_SIZE)
|
|
continue;
|
|
long cand_nsec = 0;
|
|
#ifdef __linux__
|
|
cand_nsec = st.st_mtim.tv_nsec;
|
|
#endif
|
|
if (!exact.name[0] && cand_size == check_size &&
|
|
metadata_mtime_matches(st.st_mtime, cand_nsec, check_mtime, check_mtime_nsec,
|
|
config->modify_window)) {
|
|
memcpy(exact.name, name, name_len + 1);
|
|
exact.size = cand_size;
|
|
exact.size_gap = 0;
|
|
continue;
|
|
}
|
|
/* rsync's name-distance pass: a weighted Levenshtein distance over the full
|
|
basenames, plus ten times the same distance over the filename suffixes,
|
|
accepted only when it does not exceed the running lowest distance. */
|
|
int name_suf_len = 0;
|
|
const char* name_suf = fuzzy_find_suffix(name, (int)name_len, &name_suf_len);
|
|
uint32_t distance = fuzzy_distance(name, (unsigned)name_len, leaf, (unsigned)target_len,
|
|
lowest_dist, dist_scratch);
|
|
if (distance < 0xFFFF0000U)
|
|
distance += fuzzy_distance(name_suf, (unsigned)name_suf_len, fname_suf,
|
|
(unsigned)fname_suf_len, 0xFFFF0000U, dist_scratch) *
|
|
10;
|
|
if (distance > lowest_dist)
|
|
continue;
|
|
lowest_dist = distance;
|
|
FuzzyCandidate cand;
|
|
memcpy(cand.name, name, name_len + 1);
|
|
cand.size = cand_size;
|
|
cand.distance = distance;
|
|
cand.size_gap = cand_size > check_size ? cand_size - check_size : check_size - cand_size;
|
|
if (fuzzy_candidate_better(&cand, &best))
|
|
best = cand;
|
|
}
|
|
closedir(dir);
|
|
free(leaf);
|
|
free(dist_scratch);
|
|
|
|
/* Prefer the exact size+mtime candidate over any name-distance winner. */
|
|
if (exact.name[0])
|
|
best = exact;
|
|
|
|
void* basis = NULL;
|
|
if (best.name[0]) {
|
|
/* O_NONBLOCK: a name raced to a FIFO between the fstatat gate and this open
|
|
would otherwise block the receive thread forever on open(2); with it the
|
|
open fails (ENXIO) and the fstat/S_ISREG gate below would reject it too.
|
|
A regular file opened with O_NONBLOCK is unaffected. */
|
|
int fd = openat(dir_fd, best.name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
|
|
if (fd >= 0) {
|
|
struct stat st;
|
|
if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode) &&
|
|
(unsigned long long)st.st_size == best.size && best.size <= SIZE_MAX) {
|
|
basis = protocol_alloc((size_t)best.size);
|
|
if (basis) {
|
|
size_t got = 0;
|
|
while (got < (size_t)best.size) {
|
|
ssize_t n = read(fd, (char*)basis + got, (size_t)best.size - got);
|
|
if (n <= 0) {
|
|
free(basis);
|
|
basis = NULL;
|
|
break;
|
|
}
|
|
got += (size_t)n;
|
|
}
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
}
|
|
close(dir_fd);
|
|
free(full_path);
|
|
if (basis)
|
|
*out_size = best.size;
|
|
return basis;
|
|
}
|
|
|
|
/* Read the remainder of a full-file transfer after the receiver has already
|
|
* sent STATUS_NEXT: receive the metadata frame (when enabled) followed by the
|
|
* data frame, and return an owned File. Shared by the plain full-transfer path
|
|
* and the --append-verify prefix-mismatch fallback (a clean full transfer
|
|
* instead of a corrupt prefix+tail blend). */
|
|
static File* receive_full_file(int fd, const Config* config, const char* path) {
|
|
File* file = file_create(path);
|
|
if (!file)
|
|
return NULL;
|
|
if (config->use_metadata) {
|
|
int meta_ok = 1;
|
|
file->metadata = metadata_receive(fd, &meta_ok);
|
|
if (!meta_ok) {
|
|
file_destroy(file);
|
|
return NULL;
|
|
}
|
|
}
|
|
if (!receive_file_xattrs(file, fd, config)) {
|
|
file_destroy(file);
|
|
return NULL;
|
|
}
|
|
Data* file_data = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
if (file_data == NULL) {
|
|
file_destroy(file);
|
|
return NULL;
|
|
}
|
|
if (config->use_compression &&
|
|
!compression_should_skip_with_suffixes(file->path, config->skip_compress_suffixes,
|
|
config->skip_compress_set ? config->skip_compress_count
|
|
: -1)) {
|
|
Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
ProtocolSession* owner = file_data->owner;
|
|
data_destroy(file_data);
|
|
if (uncompressed == NULL) {
|
|
file_destroy(file);
|
|
return NULL;
|
|
}
|
|
if (!data_charge_session(uncompressed, owner, uncompressed->size)) {
|
|
data_destroy(uncompressed);
|
|
file_destroy(file);
|
|
return NULL;
|
|
}
|
|
if (uncompressed->size > MAX_FILE_DATA_SIZE) {
|
|
data_destroy(uncompressed);
|
|
file_destroy(file);
|
|
return NULL;
|
|
}
|
|
file_data = uncompressed;
|
|
}
|
|
data_destroy(file->data);
|
|
file->data = file_data;
|
|
return file;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* receive_incremental_check() decomposition.
|
|
*
|
|
* The per-file STATUS_CHECK fast path is split into the small helpers below,
|
|
* called in order by a short linear orchestrator (receive_incremental_check_ex).
|
|
* Each helper owns one decision: request validation, secure destination open,
|
|
* metadata-only skip, server-contacting --dry-run no-mutation short-circuit,
|
|
* alternate-basis match, --append tail resume, block delta, --fuzzy basis, and
|
|
* the final "send the whole file" fallback. Every protocol send/receive and
|
|
* every resource cleanup is preserved exactly; the non-dry-run wire is
|
|
* byte-for-byte unchanged. receive_incremental_check_ex additionally exposes a
|
|
* `would_transfer` out-param for the dry-run caller; the 3-arg
|
|
* receive_incremental_check wrapper passes NULL.
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/* Owned state threaded through the helpers below. */
|
|
typedef struct {
|
|
int fd;
|
|
const Config* config;
|
|
char* check_path; /* received destination-relative path */
|
|
char* full_path; /* receive-root-prefixed destination path */
|
|
unsigned long long check_size;
|
|
long long check_mtime;
|
|
long long check_mtime_nsec;
|
|
uint8_t check_digest[CHECKSUM_MAX_DIGEST_LEN];
|
|
size_t check_digest_len;
|
|
/* Source metadata carried alongside the check frame whenever a basis dir is
|
|
configured (rsync keeps the whole file list; FastSync's sender-driven
|
|
incremental path otherwise never transmits metadata for a SKIPPED file).
|
|
A basis materialization applies these SOURCE attributes instead of the
|
|
basis inode's, matching rsync's "copy then fix attributes". */
|
|
FileMetadata* source_metadata;
|
|
bool dest_exists; /* any destination entry exists (lstat succeeded) */
|
|
bool has_old_file;
|
|
int old_fd;
|
|
struct stat old_st;
|
|
unsigned long long old_size;
|
|
void* old_data; /* snapshot of the existing destination, or NULL */
|
|
} IncrementalCheckState;
|
|
|
|
typedef enum {
|
|
INCREMENTAL_CONTINUE, /* proceed to the next helper */
|
|
INCREMENTAL_ERROR, /* protocol/validation failure: return NULL */
|
|
INCREMENTAL_SKIP, /* up to date: *skipped = true, return NULL */
|
|
INCREMENTAL_DRY_RUN, /* --dry-run resolved: flags set, return NULL */
|
|
INCREMENTAL_FILE, /* a File* was produced (out_file) */
|
|
} IncrementalCheckOutcome;
|
|
|
|
static void incremental_check_state_init(IncrementalCheckState* state, int fd,
|
|
const Config* config) {
|
|
memset(state, 0, sizeof(*state));
|
|
state->fd = fd;
|
|
state->config = config;
|
|
state->old_fd = -1;
|
|
}
|
|
|
|
/* Release every resource the helpers may have acquired. Idempotent, so it is
|
|
safe on every exit path exactly the way the original inline cleanup was. */
|
|
static void incremental_check_state_cleanup(IncrementalCheckState* state) {
|
|
free(state->old_data);
|
|
state->old_data = NULL;
|
|
if (state->old_fd >= 0)
|
|
close(state->old_fd);
|
|
state->old_fd = -1;
|
|
file_metadata_destroy(state->source_metadata);
|
|
state->source_metadata = NULL;
|
|
free(state->full_path);
|
|
state->full_path = NULL;
|
|
free(state->check_path);
|
|
state->check_path = NULL;
|
|
}
|
|
|
|
/* Receive and validate the STATUS_CHECK request frame: path, size, mtime,
|
|
nanosecond mtime, and (when negotiated) the source digest. */
|
|
static IncrementalCheckOutcome incremental_check_receive_request(IncrementalCheckState* state) {
|
|
int fd = state->fd;
|
|
const Config* config = state->config;
|
|
char* check_path = receive_wire_str(fd);
|
|
if (check_path == NULL)
|
|
return INCREMENTAL_ERROR;
|
|
state->check_path = check_path;
|
|
|
|
if (!receive_n_data(fd, &state->check_size, sizeof(state->check_size)) ||
|
|
!receive_n_data(fd, &state->check_mtime, sizeof(state->check_mtime)))
|
|
return INCREMENTAL_ERROR;
|
|
if (!receive_n_data(fd, &state->check_mtime_nsec, sizeof(state->check_mtime_nsec)) ||
|
|
state->check_mtime_nsec < 0 || state->check_mtime_nsec >= 1000000000LL) {
|
|
send_error_detail(fd, "invalid check mtime nanoseconds");
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
if ((config->checksum || config->verify_basis)) {
|
|
uint8_t wire_len;
|
|
if (!receive_n_data(fd, &wire_len, sizeof(wire_len)) || wire_len == 0 ||
|
|
wire_len > CHECKSUM_MAX_DIGEST_LEN ||
|
|
wire_len != checksum_digest_len((ChecksumAlgo)config->checksum_algo)) {
|
|
send_error_detail(fd, "invalid check digest length");
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
state->check_digest_len = wire_len;
|
|
if (!receive_n_data(fd, state->check_digest, state->check_digest_len))
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
/* The sender transmits the source metadata with every basis-configured check
|
|
so a basis hit can be materialized with the SOURCE's attributes (rsync
|
|
copies/copies-then-fixes; the receiver would otherwise only have the basis
|
|
inode's stat). The block is symmetric and consumed unconditionally here,
|
|
whether or not this file ends up as a basis hit. */
|
|
if (config_has_basis(config) && config->use_metadata) {
|
|
int meta_ok = 1;
|
|
state->source_metadata = metadata_receive(fd, &meta_ok);
|
|
if (!meta_ok)
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
|
|
/* A basis-configured run may materialize a file larger than the whole-file
|
|
payload bound: a basis hit is streamed from the basis path (bounded
|
|
buffers), so the check size is not itself an allocation. Every other
|
|
path (delta/append/full) still applies MAX_RECEIVE_WHOLE_FILE_SIZE, and a
|
|
miss simply falls through to the normal transfer with its own bound. */
|
|
if (!config_has_basis(config) && state->check_size > MAX_RECEIVE_WHOLE_FILE_SIZE) {
|
|
send_error_detail(fd, "check size exceeds receiver limit");
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
|
|
if (check_path[0] == '\0' || has_path_traversal(check_path)) {
|
|
char* escaped_path = output_escape(check_path, log_get_8_bit_output());
|
|
log_message(LOG_LEVEL_ERROR, "Invalid received check path: %s",
|
|
escaped_path ? escaped_path : "<allocation failed>");
|
|
free(escaped_path);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* Open the existing destination entry once, confined below the receive root,
|
|
and record its stat. */
|
|
static IncrementalCheckOutcome incremental_check_open_destination(IncrementalCheckState* state) {
|
|
char* full_path = path_cat(state->config->receive_root_directory, state->check_path);
|
|
if (!full_path) {
|
|
send_error_detail(state->fd, "could not build destination path");
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
state->full_path = full_path;
|
|
|
|
char* leaf = NULL;
|
|
int parent_fd = file_open_secure_parent(full_path, &leaf, false);
|
|
if (parent_fd >= 0) {
|
|
struct stat dest_st;
|
|
if (fstatat(parent_fd, leaf, &dest_st, AT_SYMLINK_NOFOLLOW) == 0)
|
|
state->dest_exists = true;
|
|
/* O_NONBLOCK: an existing FIFO at the destination must not block this
|
|
openat(); the S_ISREG gate below rejects the non-regular entry. */
|
|
state->old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
|
|
free(leaf);
|
|
close(parent_fd);
|
|
state->has_old_file = state->old_fd >= 0 && fstat(state->old_fd, &state->old_st) == 0 &&
|
|
S_ISREG(state->old_st.st_mode);
|
|
}
|
|
if (!state->has_old_file && state->old_fd >= 0) {
|
|
close(state->old_fd);
|
|
state->old_fd = -1;
|
|
}
|
|
state->old_size = state->has_old_file ? (unsigned long long)state->old_st.st_size : 0;
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* Output parity (protocol 2.23.0): when the wire config asked for it, report a
|
|
snapshot of the pre-transfer destination entry BEFORE the ordinary verdict so
|
|
the sender can render rsync-accurate -i/--out-format columns. A missing
|
|
destination is reported explicitly (existed=false) rather than omitted, so
|
|
the sender can distinguish "new" from "unknown". */
|
|
static IncrementalCheckOutcome incremental_check_report_dest_info(IncrementalCheckState* state) {
|
|
if (!state->config->report_dest_info)
|
|
return INCREMENTAL_CONTINUE;
|
|
OutputDestState info;
|
|
memset(&info, 0, sizeof(info));
|
|
info.known = true;
|
|
info.existed = state->has_old_file;
|
|
if (state->has_old_file) {
|
|
info.size = (unsigned long long)state->old_st.st_size;
|
|
info.mtime_sec = (long long)state->old_st.st_mtime;
|
|
#ifdef __linux__
|
|
info.mtime_nsec = state->old_st.st_mtim.tv_nsec;
|
|
#endif
|
|
info.mode = (uint32_t)state->old_st.st_mode;
|
|
info.uid = (int32_t)state->old_st.st_uid;
|
|
info.gid = (int32_t)state->old_st.st_gid;
|
|
}
|
|
if (!send_status(state->fd, STATUS_DEST_INFO) || !format_dest_state_send(state->fd, &info))
|
|
return INCREMENTAL_ERROR;
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* --ignore-existing short-circuit. The receiver must answer "skip" (STATUS_OK)
|
|
BEFORE the sender transmits any payload, otherwise the whole file crosses the
|
|
wire only to be discarded at write time. rsync skips an existing destination
|
|
entry regardless of its content or type, so the reply depends only on the
|
|
lstat existence probe; the ordinary --ignore-existing checks inside
|
|
file_receive remain as defense-in-depth for the frame types that have no
|
|
per-file check (directories/symlinks/specials/hard-links). */
|
|
static IncrementalCheckOutcome
|
|
incremental_check_ignore_existing(const IncrementalCheckState* state) {
|
|
if (!state->config->ignore_existing || !state->dest_exists)
|
|
return INCREMENTAL_CONTINUE;
|
|
if (!send_status(state->fd, STATUS_OK))
|
|
return INCREMENTAL_ERROR;
|
|
return INCREMENTAL_SKIP;
|
|
}
|
|
|
|
/* Metadata for a materialized basis hit: prefer the SOURCE metadata the sender
|
|
transmitted with the check frame (rsync copies then fixes the destination to
|
|
the source's attributes); fall back to the basis inode's own stat when
|
|
metadata was not negotiated. Consumes state->source_metadata on success. */
|
|
static FileMetadata* basis_take_metadata(IncrementalCheckState* state,
|
|
const struct stat* basis_st) {
|
|
if (state->source_metadata) {
|
|
FileMetadata* meta = state->source_metadata;
|
|
state->source_metadata = NULL;
|
|
return meta;
|
|
}
|
|
return file_metadata_create(NULL, basis_st, false, false);
|
|
}
|
|
|
|
/* --link-dest relink of an already up-to-date destination. rsync hard-links a
|
|
destination entry to a matching basis even when the entry is already correct,
|
|
so a run over an existing tree still maximizes sharing with the basis. Only a
|
|
link-dest basis triggers this (copy-dest/compare-dest leave an up-to-date
|
|
destination untouched, matching rsync). The ordinary basis path further down
|
|
handles every not-up-to-date case, so this helper only adds the relink that
|
|
the quick-skip would otherwise short-circuit. */
|
|
static IncrementalCheckOutcome incremental_check_link_dest_relink(IncrementalCheckState* state,
|
|
File** out_file) {
|
|
const Config* config = state->config;
|
|
if (!config_has_basis(config) || config->ignore_times || config->dry_run)
|
|
return INCREMENTAL_CONTINUE;
|
|
if (!state->has_old_file)
|
|
return INCREMENTAL_CONTINUE;
|
|
BasisMatch basis;
|
|
basis_match_find(config, state->check_path, state->check_size, (time_t)state->check_mtime,
|
|
(long)state->check_mtime_nsec, state->check_digest, state->check_digest_len,
|
|
true, &basis);
|
|
/* Only a link-dest hit relinks; a copy-dest/compare-dest hit (or a miss) lets
|
|
the up-to-date check below keep the existing destination. */
|
|
if (!basis.hit || basis.type != BASIS_DEST_LINK) {
|
|
basis_match_free(&basis);
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
/* Already the basis inode: nothing to do, leave the destination alone. */
|
|
if (basis.st.st_dev == state->old_st.st_dev && basis.st.st_ino == state->old_st.st_ino) {
|
|
basis_match_free(&basis);
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
File* materialized = file_create(state->check_path);
|
|
if (materialized) {
|
|
data_destroy(materialized->data);
|
|
materialized->data = data_create_reserve((size_t)state->check_size);
|
|
if (!materialized->data) {
|
|
file_destroy(materialized);
|
|
materialized = NULL;
|
|
}
|
|
}
|
|
if (materialized) {
|
|
materialized->metadata = basis_take_metadata(state, &basis.st);
|
|
materialized->skip = true;
|
|
materialized->basis_link = basis.basis_path;
|
|
basis.basis_path = NULL;
|
|
if (!materialized->metadata) {
|
|
file_destroy(materialized);
|
|
materialized = NULL;
|
|
}
|
|
}
|
|
if (materialized) {
|
|
if (!send_status(state->fd, STATUS_OK)) {
|
|
basis_match_free(&basis);
|
|
file_destroy(materialized);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
basis_match_free(&basis);
|
|
*out_file = materialized;
|
|
return INCREMENTAL_FILE;
|
|
}
|
|
basis_match_free(&basis);
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* Metadata-only (and, when --checksum forces it, content) up-to-date decision.
|
|
Loads the old contents only when a checksum comparison or delta needs them. */
|
|
static IncrementalCheckOutcome incremental_check_quick_skip(IncrementalCheckState* state,
|
|
bool* out_try_delta) {
|
|
int fd = state->fd;
|
|
const Config* config = state->config;
|
|
bool has_old_file = state->has_old_file;
|
|
unsigned long long old_size = state->old_size;
|
|
struct stat st = state->old_st;
|
|
|
|
bool size_equal = has_old_file && old_size == state->check_size;
|
|
bool match_by_metadata = false;
|
|
if (size_equal && !config->ignore_times && !config->size_only) {
|
|
long long old_mtime_nsec = 0;
|
|
#ifdef __linux__
|
|
old_mtime_nsec = st.st_mtim.tv_nsec;
|
|
#endif
|
|
match_by_metadata =
|
|
metadata_mtime_matches(st.st_mtime, old_mtime_nsec, (time_t)state->check_mtime,
|
|
(long)state->check_mtime_nsec, config->modify_window);
|
|
}
|
|
|
|
bool try_delta = config->use_delta && !config->whole_file && has_old_file &&
|
|
delta_should_attempt(old_size, state->check_size, config->delta_max_file_size);
|
|
bool checksum_needs_read = size_equal && !config->ignore_times && config->checksum;
|
|
/* --dry-run must never read the destination file's CONTENTS: a client could
|
|
otherwise use `--dry-run --checksum` against a read-only module as a
|
|
1-bit content oracle (hash match / mismatch) and force arbitrary reads.
|
|
Decide from metadata alone; when metadata is inconclusive (checksum or
|
|
delta would have required the body) report would-transfer. The real
|
|
(non-dry-run) behavior below is unchanged. */
|
|
bool need_old_data = !config->dry_run && (checksum_needs_read || try_delta);
|
|
if (config->dry_run)
|
|
try_delta = false;
|
|
*out_try_delta = try_delta;
|
|
|
|
if (need_old_data && has_old_file && old_size > 0 && old_size <= MAX_RECEIVE_WHOLE_FILE_SIZE &&
|
|
old_size <= SIZE_MAX) {
|
|
state->old_data = protocol_alloc((size_t)old_size);
|
|
if (state->old_data) {
|
|
size_t got = 0;
|
|
while (got < (size_t)old_size) {
|
|
ssize_t n = read(state->old_fd, (char*)state->old_data + got, (size_t)old_size - got);
|
|
if (n <= 0) {
|
|
free(state->old_data);
|
|
state->old_data = NULL;
|
|
break;
|
|
}
|
|
got += (size_t)n;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool match = false;
|
|
if (config->dry_run) {
|
|
/* Metadata-only decision: a size match plus a matching mtime is treated as
|
|
up to date; --checksum/--delta cannot be verified without reading, so an
|
|
otherwise inconclusive comparison is a would-transfer. */
|
|
match = size_equal && !config->ignore_times && (config->size_only || match_by_metadata);
|
|
} else if (checksum_needs_read) {
|
|
uint8_t old_digest[CHECKSUM_MAX_DIGEST_LEN];
|
|
size_t old_len = 0;
|
|
bool hashed = checksum_digest((ChecksumAlgo)config->checksum_algo, config->checksum_seed,
|
|
old_size == 0 ? "" : state->old_data, (size_t)old_size,
|
|
old_digest, sizeof(old_digest), &old_len);
|
|
match = hashed && old_len == state->check_digest_len && state->check_digest_len > 0 &&
|
|
memcmp(old_digest, state->check_digest, state->check_digest_len) == 0;
|
|
} else if (size_equal && !config->ignore_times) {
|
|
match = config->size_only || match_by_metadata;
|
|
}
|
|
|
|
if (match) {
|
|
if (!send_status(fd, STATUS_OK))
|
|
return INCREMENTAL_ERROR;
|
|
return INCREMENTAL_SKIP;
|
|
}
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* Server-contacting --dry-run no-mutation short-circuit. Runs after the
|
|
quick-skip decision and before any path that could touch the destination.
|
|
When dry_run is set and the file is not already up to date the receiver must
|
|
materialize nothing (no basis link/copy, no append/delta/full transfer) and
|
|
the sender must send no data, so answer STATUS_DRY_RUN_TRANSFER and stop.
|
|
|
|
The basis lookup is content-blind: under the default metadata quick-check a
|
|
hit needs no basis bytes and is honored here just as in a real run; under
|
|
--verify-basis a real run hashes the basis against the client-supplied digest,
|
|
which in a dry-run is a 1-bit content oracle, so no basis bytes may be read
|
|
and an otherwise-matching entry is reported as would-transfer. Everything
|
|
read here (the destination file's metadata, basis candidates' metadata) is
|
|
read-only. */
|
|
static IncrementalCheckOutcome incremental_check_dry_run_shortcut(IncrementalCheckState* state,
|
|
bool* skipped,
|
|
bool* would_transfer) {
|
|
const Config* config = state->config;
|
|
if (!config->dry_run)
|
|
return INCREMENTAL_CONTINUE;
|
|
|
|
bool skip_via_compare = false;
|
|
if (config_has_basis(config) && !config->ignore_times) {
|
|
BasisMatch basis;
|
|
/* hash_content=false: a dry-run must not read or hash the basis file, so
|
|
under --verify-basis no compare-dest hit can be confirmed and an
|
|
otherwise-matching file is reported as would-transfer. Without
|
|
--verify-basis the metadata quick-check confirms it without touching any
|
|
basis bytes. */
|
|
basis_match_find(config, state->check_path, state->check_size, (time_t)state->check_mtime,
|
|
(long)state->check_mtime_nsec, state->check_digest, state->check_digest_len,
|
|
false, &basis);
|
|
if (basis.hit && basis.type == BASIS_DEST_COMPARE && !state->has_old_file)
|
|
skip_via_compare = true;
|
|
basis_match_free(&basis);
|
|
}
|
|
Status reply = skip_via_compare ? STATUS_OK : STATUS_DRY_RUN_TRANSFER;
|
|
if (!send_status(state->fd, reply))
|
|
return INCREMENTAL_ERROR;
|
|
if (skip_via_compare)
|
|
*skipped = true;
|
|
else if (would_transfer)
|
|
*would_transfer = true;
|
|
return INCREMENTAL_DRY_RUN;
|
|
}
|
|
|
|
/* Alternate basis directories (--compare-dest/--copy-dest/--link-dest): a hit
|
|
either suppresses the transfer (compare-dest) or materializes the file from
|
|
the basis without a data frame. */
|
|
static IncrementalCheckOutcome incremental_check_try_basis(IncrementalCheckState* state,
|
|
File** out_file) {
|
|
int fd = state->fd;
|
|
const Config* config = state->config;
|
|
if (!config_has_basis(config))
|
|
return INCREMENTAL_CONTINUE;
|
|
|
|
BasisMatch basis;
|
|
basis_match_find(config, state->check_path, state->check_size, (time_t)state->check_mtime,
|
|
(long)state->check_mtime_nsec, state->check_digest, state->check_digest_len,
|
|
true, &basis);
|
|
if (basis.hit) {
|
|
if (basis.type == BASIS_DEST_COMPARE) {
|
|
basis_match_free(&basis);
|
|
if (!state->has_old_file) {
|
|
if (!send_status(fd, STATUS_OK))
|
|
return INCREMENTAL_ERROR;
|
|
return INCREMENTAL_SKIP;
|
|
}
|
|
} else {
|
|
/* Copy/link installs source their bytes from the basis PATH at install
|
|
time (bounded buffers), so no whole-file content buffer is needed here
|
|
even for an over-limit basis. */
|
|
File* materialized = file_create(state->check_path);
|
|
if (materialized) {
|
|
data_destroy(materialized->data);
|
|
materialized->data = data_create_reserve((size_t)state->check_size);
|
|
if (!materialized->data) {
|
|
file_destroy(materialized);
|
|
materialized = NULL;
|
|
}
|
|
}
|
|
if (materialized) {
|
|
materialized->metadata = basis_take_metadata(state, &basis.st);
|
|
materialized->skip = true; /* receiver must not ack this as a data file */
|
|
if (basis.type == BASIS_DEST_LINK)
|
|
materialized->basis_link = basis.basis_path;
|
|
else
|
|
materialized->basis_copy = basis.basis_path;
|
|
basis.basis_path = NULL;
|
|
if (!materialized->metadata) {
|
|
file_destroy(materialized);
|
|
materialized = NULL;
|
|
}
|
|
}
|
|
if (materialized) {
|
|
if (!send_status(fd, STATUS_OK)) {
|
|
basis_match_free(&basis);
|
|
file_destroy(materialized);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
basis_match_free(&basis);
|
|
*out_file = materialized;
|
|
return INCREMENTAL_FILE;
|
|
}
|
|
/* Materialization setup failed: fall through to the normal transfer. */
|
|
}
|
|
}
|
|
basis_match_free(&basis);
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* --append / --append-verify tail resume: when the destination is a SHORTER
|
|
file in an append mode, negotiate the resume offset and receive only the
|
|
tail. Produces the reconstructed file, or falls through to delta/full. */
|
|
static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCheckState* state,
|
|
File** out_file) {
|
|
int fd = state->fd;
|
|
const Config* config = state->config;
|
|
const char* check_path = state->check_path;
|
|
unsigned long long old_size = state->old_size;
|
|
unsigned long long check_size = state->check_size;
|
|
|
|
bool append_resume = (config->append || config->append_verify) && state->has_old_file &&
|
|
append_resume_eligible(old_size, check_size);
|
|
if (!append_resume)
|
|
return INCREMENTAL_CONTINUE;
|
|
|
|
/* Ensure the retained prefix (== the whole, shorter destination file) is in
|
|
memory; it is needed both to rebuild the full file and, for
|
|
--append-verify, to checksum it. A load failure is not fatal: the resume is
|
|
simply not possible and we fall through to the other paths. */
|
|
if (state->old_data == NULL && old_size > 0 && old_size <= MAX_RECEIVE_WHOLE_FILE_SIZE &&
|
|
old_size <= SIZE_MAX) {
|
|
state->old_data = protocol_alloc((size_t)old_size);
|
|
if (state->old_data) {
|
|
size_t got = 0;
|
|
while (got < (size_t)old_size) {
|
|
ssize_t n = read(state->old_fd, (char*)state->old_data + got, (size_t)old_size - got);
|
|
if (n <= 0) {
|
|
free(state->old_data);
|
|
state->old_data = NULL;
|
|
break;
|
|
}
|
|
got += (size_t)n;
|
|
}
|
|
}
|
|
}
|
|
if (state->old_data == NULL && old_size != 0)
|
|
return INCREMENTAL_CONTINUE;
|
|
|
|
if (!send_status(fd, STATUS_APPEND) || !send_n_data(fd, &old_size, sizeof(old_size)))
|
|
return INCREMENTAL_ERROR;
|
|
bool verify = config->append_verify;
|
|
bool full_fallback = false;
|
|
if (verify) {
|
|
Status sig_status;
|
|
if (!receive_status(fd, &sig_status))
|
|
return INCREMENTAL_ERROR;
|
|
if (sig_status != STATUS_APPEND_SIG) {
|
|
send_status(fd, STATUS_ERROR);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
uint64_t src_prefix_hash;
|
|
if (!receive_n_data(fd, &src_prefix_hash, sizeof(src_prefix_hash)))
|
|
return INCREMENTAL_ERROR;
|
|
/* Compare the retained prefix against the source prefix. A mismatch must
|
|
never be silently appended to: fall back to a full transfer so the result
|
|
is a byte-identical source copy. */
|
|
uint64_t dst_prefix_hash =
|
|
old_size == 0 ? delta_xxhash64("", 0) : delta_xxhash64(state->old_data, (size_t)old_size);
|
|
if (dst_prefix_hash == src_prefix_hash) {
|
|
if (!send_status(fd, STATUS_APPEND_OK))
|
|
return INCREMENTAL_ERROR;
|
|
} else {
|
|
if (!send_status(fd, STATUS_NEXT))
|
|
return INCREMENTAL_ERROR;
|
|
full_fallback = true;
|
|
}
|
|
}
|
|
|
|
if (full_fallback) {
|
|
/* Retained prefix differed: receive the sender's full transfer. */
|
|
free(state->old_data);
|
|
state->old_data = NULL;
|
|
if (state->old_fd >= 0) {
|
|
close(state->old_fd);
|
|
state->old_fd = -1;
|
|
}
|
|
*out_file = receive_full_file(fd, config, check_path);
|
|
return INCREMENTAL_FILE;
|
|
}
|
|
|
|
/* Receive the tail (STATUS_APPEND_DATA + metadata + tail bytes). */
|
|
Status tail_status;
|
|
if (!receive_status(fd, &tail_status))
|
|
return INCREMENTAL_ERROR;
|
|
if (tail_status != STATUS_APPEND_DATA) {
|
|
send_status(fd, STATUS_ERROR);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
FileMetadata* meta = NULL;
|
|
FileXattrList* append_xattrs = NULL;
|
|
if (config->use_metadata) {
|
|
int meta_ok = 1;
|
|
meta = metadata_receive(fd, &meta_ok);
|
|
if (!meta_ok)
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
if (config->use_xattrs) {
|
|
int xok = 0;
|
|
append_xattrs = xattr_receive(fd, &xok, config->preserve_acls);
|
|
if (!xok) {
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
}
|
|
Data* tail = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
if (tail == NULL) {
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
if (config->use_compression &&
|
|
!compression_should_skip_with_suffixes(check_path, config->skip_compress_suffixes,
|
|
config->skip_compress_set ? config->skip_compress_count
|
|
: -1)) {
|
|
Data* uncompressed = data_decompress_limited(tail, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
|
ProtocolSession* owner = tail->owner;
|
|
data_destroy(tail);
|
|
if (uncompressed == NULL) {
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
if (!data_charge_session(uncompressed, owner, uncompressed->size)) {
|
|
data_destroy(uncompressed);
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
if (uncompressed->size > MAX_FILE_DATA_SIZE) {
|
|
data_destroy(uncompressed);
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
tail = uncompressed;
|
|
}
|
|
/* The tail must complete the file exactly; anything else is a protocol
|
|
violation (never a truncated or overrun file). */
|
|
unsigned long long expected_tail;
|
|
if (!append_tail_length(old_size, check_size, &expected_tail) ||
|
|
tail->size != (size_t)expected_tail) {
|
|
send_status(fd, STATUS_ERROR);
|
|
data_destroy(tail);
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
size_t full_size = (size_t)check_size;
|
|
void* full = protocol_alloc(full_size ? full_size : 1);
|
|
if (!full) {
|
|
data_destroy(tail);
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
if (old_size > 0 && state->old_data)
|
|
memcpy(full, state->old_data, (size_t)old_size);
|
|
if (tail->size > 0)
|
|
memcpy((char*)full + old_size, tail->data, tail->size);
|
|
data_destroy(tail);
|
|
free(state->old_data);
|
|
state->old_data = NULL;
|
|
|
|
File* file = file_create(check_path);
|
|
if (!file) {
|
|
free(full);
|
|
xattr_list_free(append_xattrs);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
file->metadata = meta;
|
|
file->xattrs = append_xattrs;
|
|
append_xattrs = NULL;
|
|
data_destroy(file->data);
|
|
file->data = data_create(full, full_size);
|
|
if (!file->data) { /* data_create already freed full on failure */
|
|
file_destroy(file);
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
*out_file = file;
|
|
return INCREMENTAL_FILE;
|
|
}
|
|
|
|
/* Block delta transfer against the existing destination content. */
|
|
static IncrementalCheckOutcome incremental_check_try_delta(IncrementalCheckState* state,
|
|
bool try_delta, File** out_file) {
|
|
if (try_delta && state->old_data != NULL) {
|
|
bool delta_failed = false;
|
|
File* delta_file = receive_delta_file(state->fd, state->config, state->check_path,
|
|
state->old_data, state->old_size, &delta_failed);
|
|
state->old_data = NULL; /* receive_delta_file consumes the snapshot on every path */
|
|
if (delta_file) {
|
|
*out_file = delta_file;
|
|
return INCREMENTAL_FILE;
|
|
}
|
|
if (delta_failed)
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
free(state->old_data);
|
|
state->old_data = NULL;
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* -y/--fuzzy similar-file delta basis. Reaching this point means the file
|
|
must be transferred and the destination's own content could not serve as a
|
|
delta basis; try an existing similar-named sibling in the same directory. */
|
|
static IncrementalCheckOutcome incremental_check_try_fuzzy(IncrementalCheckState* state,
|
|
File** out_file) {
|
|
const Config* config = state->config;
|
|
if (!config->fuzzy || !config->use_delta)
|
|
return INCREMENTAL_CONTINUE;
|
|
unsigned long long fuzzy_size = 0;
|
|
void* fuzzy_basis = fuzzy_basis_find_and_load(config, state->check_path, state->check_size,
|
|
(time_t)state->check_mtime,
|
|
(long)state->check_mtime_nsec, &fuzzy_size);
|
|
if (fuzzy_basis != NULL) {
|
|
bool fuzzy_failed = false;
|
|
File* fuzzy_file = receive_delta_file(state->fd, config, state->check_path, fuzzy_basis,
|
|
fuzzy_size, &fuzzy_failed);
|
|
fuzzy_basis = NULL; /* receive_delta_file consumes the buffer on every path */
|
|
if (fuzzy_file) {
|
|
*out_file = fuzzy_file;
|
|
return INCREMENTAL_FILE;
|
|
}
|
|
if (fuzzy_failed)
|
|
return INCREMENTAL_ERROR;
|
|
}
|
|
free(fuzzy_basis);
|
|
return INCREMENTAL_CONTINUE;
|
|
}
|
|
|
|
/* Final fallback: tell the sender to transmit the whole file and receive it. */
|
|
static File* incremental_check_receive_full(IncrementalCheckState* state) {
|
|
if (!send_status(state->fd, STATUS_NEXT))
|
|
return NULL;
|
|
if (state->old_fd >= 0) {
|
|
close(state->old_fd);
|
|
state->old_fd = -1;
|
|
}
|
|
return receive_full_file(state->fd, state->config, state->check_path);
|
|
}
|
|
|
|
/* Core implementation. `would_transfer` (may be NULL) is set true only on the
|
|
* server-contacting --dry-run path, when the file is not up to date and the
|
|
* receiver answered STATUS_DRY_RUN_TRANSFER; the caller then knows no File is
|
|
* returned and nothing was stored. */
|
|
File* receive_incremental_check_ex(int fd, const Config* config, bool* skipped,
|
|
bool* would_transfer) {
|
|
if (would_transfer)
|
|
*would_transfer = false;
|
|
if (!config || !skipped) {
|
|
send_status(fd, STATUS_ERROR);
|
|
return NULL;
|
|
}
|
|
*skipped = false;
|
|
|
|
IncrementalCheckState state;
|
|
incremental_check_state_init(&state, fd, config);
|
|
|
|
File* result = NULL;
|
|
bool try_delta = false;
|
|
IncrementalCheckOutcome outcome;
|
|
|
|
outcome = incremental_check_receive_request(&state);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
|
|
outcome = incremental_check_open_destination(&state);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
|
|
outcome = incremental_check_report_dest_info(&state);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
|
|
/* --ignore-existing must answer before any data is requested; it takes
|
|
precedence over the metadata up-to-date check below. */
|
|
outcome = incremental_check_ignore_existing(&state);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_SKIP) {
|
|
*skipped = true;
|
|
goto done;
|
|
}
|
|
|
|
/* A --link-dest hit relinks even an already up-to-date destination before the
|
|
quick-skip can suppress it (rsync parity). */
|
|
outcome = incremental_check_link_dest_relink(&state, &result);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_FILE)
|
|
goto done;
|
|
|
|
outcome = incremental_check_quick_skip(&state, &try_delta);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_SKIP) {
|
|
*skipped = true;
|
|
goto done;
|
|
}
|
|
|
|
/* Dry-run resolves here (no mutation) or falls through to the normal path. */
|
|
outcome = incremental_check_dry_run_shortcut(&state, skipped, would_transfer);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome != INCREMENTAL_CONTINUE)
|
|
goto done;
|
|
|
|
outcome = incremental_check_try_basis(&state, &result);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_SKIP) {
|
|
*skipped = true;
|
|
goto done;
|
|
}
|
|
if (outcome == INCREMENTAL_FILE)
|
|
goto done;
|
|
|
|
outcome = incremental_check_try_append_resume(&state, &result);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_FILE)
|
|
goto done;
|
|
|
|
outcome = incremental_check_try_delta(&state, try_delta, &result);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_FILE)
|
|
goto done;
|
|
|
|
outcome = incremental_check_try_fuzzy(&state, &result);
|
|
if (outcome == INCREMENTAL_ERROR)
|
|
goto done;
|
|
if (outcome == INCREMENTAL_FILE)
|
|
goto done;
|
|
|
|
result = incremental_check_receive_full(&state);
|
|
|
|
done:
|
|
incremental_check_state_cleanup(&state);
|
|
return result;
|
|
}
|
|
|
|
File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|
return receive_incremental_check_ex(fd, config, skipped, NULL);
|
|
}
|