fix(parity): receiver-side --ignore-existing short-circuit before payload

This commit is contained in:
opencode
2026-09-16 22:29:46 +02:00
parent 2b5aaef409
commit 3e9f70d9ba
2 changed files with 38 additions and 0 deletions
+9
View File
@@ -2349,6 +2349,15 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
config->preserve_times = true;
}
/* --ignore-existing is a receiver-side existence policy: the receiver must
* answer "skip" BEFORE the sender transmits any payload, which only the
* per-file STATUS_CHECK handshake provides. Imply --incremental here (after
* the auto-preserve capture above, so a bare --ignore-existing does not gain
* -p/-t, which rsync likewise does not imply) so an existing destination is
* skipped on the wire instead of being streamed and discarded. */
if (config->ignore_existing)
config->use_incremental = true;
/* Derive the transport bit from the FINAL parsed flags. Every
* preservation/ownership option that needs the metadata frame (per-attribute
* perms/times/owner/group, atimes/crtimes, executability, xattrs/acls,
+29
View File
@@ -1764,6 +1764,7 @@ typedef struct {
long long check_mtime_nsec;
uint8_t check_digest[CHECKSUM_MAX_DIGEST_LEN];
size_t check_digest_len;
bool dest_exists; /* any destination entry exists (lstat succeeded) */
bool has_old_file;
int old_fd;
struct stat old_st;
@@ -1860,6 +1861,9 @@ static IncrementalCheckOutcome incremental_check_open_destination(IncrementalChe
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);
@@ -1903,6 +1907,21 @@ static IncrementalCheckOutcome incremental_check_report_dest_info(IncrementalChe
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(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-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,
@@ -2351,6 +2370,16 @@ File* receive_incremental_check_ex(int fd, const Config* config, bool* skipped,
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;
}
outcome = incremental_check_quick_skip(&state, &try_delta);
if (outcome == INCREMENTAL_ERROR)
goto done;