quality: refactors (#321-#326) + fake-super directories (#319) #329

Merged
TapTap merged 14 commits from quality/refactor into dev 2026-09-24 03:17:42 +02:00
2 changed files with 420 additions and 321 deletions
Showing only changes of commit 2a7afbda0a - Show all commits
+254 -182
View File
@@ -224,6 +224,235 @@ typedef struct {
void* observer_context; /* DELETE mode */ void* observer_context; /* DELETE mode */
} DeleteWalkState; } DeleteWalkState;
/* The per-walk invariants threaded unchanged through every recursive descent:
the keep/synchronized-dir indexes, the destination mode and the protection
rules. Bundling them keeps the recursive helpers below to a handful of
positional arguments. */
typedef struct {
const PathIndex* keep;
const PathIndex* dirs;
DeleteWalkState* state;
const DeleteSkipEntry* skips;
int skip_count;
const DeleteProtectRules* protect;
} DeleteWalkContext;
/* Duplicate `path` with rsync's trailing-slash convention, used to report a
removed (or would-be-removed) directory. Returns NULL on allocation
failure. */
static char* with_trailing_slash(const char* path) {
size_t len = strlen(path);
char* copy = malloc(len + 2);
if (!copy)
return NULL;
memcpy(copy, path, len);
copy[len] = '/';
copy[len + 1] = '\0';
return copy;
}
/* Forward declaration: the ordered passes below recurse through the driver. */
static bool delete_walk_fd(int dirfd, const char* rel_path, const DeleteWalkContext* ctx,
bool parent_deletable, bool* all_removed);
/* Descend into the child directory `name` of `dirfd`, walking it as part of the
current operation. Returns false on a genuine open/walk failure; on success
*child_all_removed reports whether the child removed everything it held (so
the caller may rmdir it). */
static bool delete_walk_child(int dirfd, const char* name, const char* child_rel,
const DeleteWalkContext* ctx, bool deletable,
bool* child_all_removed) {
*child_all_removed = false;
int childfd = openat(dirfd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (childfd < 0)
return errno == ENOENT;
bool ok = delete_walk_fd(childfd, child_rel, ctx, deletable, child_all_removed);
close(childfd);
return ok;
}
/* Classify every entry up front (the verdict does not depend on processing
order) so the ordered passes below can act on it. Sets shielded[]/is_extra[]
and reports through *local_survives whether anything in this directory stays
in place. Returns false on a path-construction failure. */
static bool delete_walk_classify(const char* rel_path, const DeleteDirEntry* entries, size_t count,
const DeleteWalkContext* ctx, bool deletable, bool at_root,
bool* shielded, bool* is_extra, bool* local_survives) {
bool ok = true;
for (size_t i = 0; i < count; i++) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
/* A --delay-updates run keeps its staging directory as a direct child of
the receive root, and basis-dir snapshots live below it too. Their
contents are not manifest entries, so descending into them would delete
every staged / basis file as an "extra". Only the staging name (a
top-level-only prefix) and the basis prefixes are protected: a nested
destination directory that happens to be called .fastsync-stage is
ordinary content. */
if (path_under_skip_prefix(child_rel, at_root, ctx->skips, ctx->skip_count)) {
shielded[i] = true;
*local_survives = true;
} else if (delete_protect_verdict(ctx->protect, child_rel, entries[i].name,
entries[i].is_dir) == FILTER_ACTION_PROTECT) {
/* A first-match protect rule shields the extra; for a directory the whole
subtree is shielded (rsync prunes an excluded directory), so do not
descend. */
shielded[i] = true;
*local_survives = true;
} else if (entries[i].is_dir) {
bool child_synced = ctx->dirs && path_index_contains(ctx->dirs, child_rel);
is_extra[i] = deletable && !child_synced && !keep_is_dir(ctx->keep, child_rel);
if (!is_extra[i])
*local_survives = true;
} else {
is_extra[i] = deletable && !keep_is_file(ctx->keep, child_rel);
if (!is_extra[i])
*local_survives = true;
}
free(child_rel);
}
return ok;
}
/* Pass 1: extraneous subdirectories, descending. Recurses into each and, when
the child removed everything it held, records or removes it and charges the
budget. */
static bool delete_walk_extra_dirs(int dirfd, const char* rel_path, const DeleteDirEntry* entries,
size_t dir_count, const DeleteWalkContext* ctx, bool deletable,
const bool* is_extra, bool* local_survives) {
bool ok = true;
for (size_t i = 0; i < dir_count; i++) {
if (!is_extra[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
bool child_all_removed = false;
if (!delete_walk_child(dirfd, entries[i].name, child_rel, ctx, deletable, &child_all_removed))
ok = false;
if (child_all_removed && deletable) {
if (ctx->state->mode == DELETE_WALK_MODE_LIST) {
/* Record the directory with rsync's trailing slash. */
char* copy = with_trailing_slash(child_rel);
if (!copy) {
ok = false;
} else if (!array_list_add(ctx->state->out, copy)) {
free(copy);
ok = false;
} else {
(*ctx->state->recorded)++;
}
} else if (ctx->state->budget->deleted >= ctx->state->budget->max_delete) {
ctx->state->budget->limit_hit = true;
ctx->state->budget->skipped++;
*local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, AT_REMOVEDIR) != 0) {
/* ENOENT: already gone (fine). ENOTEMPTY/EEXIST: the directory still
holds entries the walker leaves in place (a protected excluded
prefix, a kept file the manifest protects, a symlink); rsync leaves
such a directory behind, so this is not an error. Only genuine I/O
failures abort the deletion. */
if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST)
ok = false;
*local_survives = true;
} else {
ctx->state->budget->deleted++;
/* rsync reports a removed directory with a trailing slash. */
if (ctx->state->observer) {
char* with_slash = with_trailing_slash(child_rel);
if (with_slash) {
ctx->state->observer(ctx->state->observer_context, with_slash, DELETE_ENTRY_DIR);
free(with_slash);
} else {
ctx->state->observer(ctx->state->observer_context, child_rel, DELETE_ENTRY_DIR);
}
}
}
} else {
*local_survives = true;
}
free(child_rel);
}
return ok;
}
/* Pass 2: extraneous files, descending. */
static bool delete_walk_extra_files(int dirfd, const char* rel_path, const DeleteDirEntry* entries,
size_t dir_count, size_t count, const DeleteWalkContext* ctx,
const bool* is_extra, bool* local_survives) {
bool ok = true;
for (size_t i = dir_count; i < count; i++) {
if (!is_extra[i])
continue;
if (ctx->state->mode == DELETE_WALK_MODE_LIST) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
char* copy = str_dup(child_rel);
if (!copy || !array_list_add(ctx->state->out, copy)) {
free(copy);
ok = false;
} else {
(*ctx->state->recorded)++;
}
free(child_rel);
} else if (ctx->state->budget->deleted >= ctx->state->budget->max_delete) {
ctx->state->budget->limit_hit = true;
ctx->state->budget->skipped++;
*local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, 0) != 0) {
if (errno != ENOENT)
ok = false;
*local_survives = true;
} else {
ctx->state->budget->deleted++;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (child_rel) {
if (ctx->state->observer)
ctx->state->observer(ctx->state->observer_context, child_rel,
delete_entry_type_of_mode(entries[i].mode));
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
}
free(child_rel);
}
}
return ok;
}
/* Pass 3: kept subdirectories, ascending (rsync descends into these only after
the parent's own extras have been handled). */
static bool delete_walk_kept_dirs(int dirfd, const char* rel_path, const DeleteDirEntry* entries,
size_t dir_count, const DeleteWalkContext* ctx, bool deletable,
const bool* is_extra, const bool* shielded,
bool* local_survives) {
bool ok = true;
for (size_t i = dir_count; i-- > 0;) {
if (is_extra[i] || shielded[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
bool child_all_removed = false;
if (!delete_walk_child(dirfd, entries[i].name, child_rel, ctx, deletable, &child_all_removed))
ok = false;
/* A kept/synchronized directory is never removed. */
*local_survives = true;
free(child_rel);
}
return ok;
}
/* Remove the extras directly inside the directory open on `dirfd` (DELETE mode) /* Remove the extras directly inside the directory open on `dirfd` (DELETE mode)
or record the paths that WOULD be removed (LIST mode), recursing into every or record the paths that WOULD be removed (LIST mode), recursing into every
child directory so kept content below a synchronized prefix is reached. child directory so kept content below a synchronized prefix is reached.
@@ -238,11 +467,8 @@ typedef struct {
descending name order, then extraneous files, then kept subdirectories in descending name order, then extraneous files, then kept subdirectories in
ascending order) rather than readdir() order, so `--max-delete` leaves the ascending order) rather than readdir() order, so `--max-delete` leaves the
same survivors and the `--info=del`/dry-run line order matches rsync. */ same survivors and the `--info=del`/dry-run line order matches rsync. */
static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* keep, static bool delete_walk_fd(int dirfd, const char* rel_path, const DeleteWalkContext* ctx,
const PathIndex* dirs, DeleteWalkState* state, bool parent_deletable, bool* all_removed) {
const DeleteSkipEntry* skips, int skip_count,
const DeleteProtectRules* protect, bool parent_deletable,
bool* all_removed) {
DeleteDirEntry* entries = NULL; DeleteDirEntry* entries = NULL;
size_t count = 0; size_t count = 0;
bool collect_ok = true; bool collect_ok = true;
@@ -261,7 +487,7 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
/* A directory is deletable when it or ANY ancestor is synchronized; the /* A directory is deletable when it or ANY ancestor is synchronized; the
`parent_deletable` flag carries that down the recursion so dest-only `parent_deletable` flag carries that down the recursion so dest-only
directories below a synchronized root are removed wholesale. */ directories below a synchronized root are removed wholesale. */
bool deletable = parent_deletable || is_synced_dir(dirs, rel_path); bool deletable = parent_deletable || is_synced_dir(ctx->dirs, rel_path);
bool at_root = rel_path[0] == '\0'; bool at_root = rel_path[0] == '\0';
/* Reproduce rsync's traversal order: extraneous subdirectories in descending /* Reproduce rsync's traversal order: extraneous subdirectories in descending
@@ -274,182 +500,18 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
while (dir_count < count && entries[dir_count].is_dir) while (dir_count < count && entries[dir_count].is_dir)
dir_count++; dir_count++;
/* Classify every entry up front (the verdict does not depend on processing if (!delete_walk_classify(rel_path, entries, count, ctx, deletable, at_root, shielded, is_extra,
order) so the ordered passes below can act on it. */ &local_survives))
for (size_t i = 0; i < count; i++) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false; operation_ok = false;
continue; if (!delete_walk_extra_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
} &local_survives))
/* A --delay-updates run keeps its staging directory as a direct child of
the receive root, and basis-dir snapshots live below it too. Their
contents are not manifest entries, so descending into them would delete
every staged / basis file as an "extra". Only the staging name (a
top-level-only prefix) and the basis prefixes are protected: a nested
destination directory that happens to be called .fastsync-stage is
ordinary content. */
if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) {
shielded[i] = true;
local_survives = true;
} else if (delete_protect_verdict(protect, child_rel, entries[i].name, entries[i].is_dir) ==
FILTER_ACTION_PROTECT) {
/* A first-match protect rule shields the extra; for a directory the whole
subtree is shielded (rsync prunes an excluded directory), so do not
descend. */
shielded[i] = true;
local_survives = true;
} else if (entries[i].is_dir) {
bool child_synced = dirs && path_index_contains(dirs, child_rel);
is_extra[i] = deletable && !child_synced && !keep_is_dir(keep, child_rel);
if (!is_extra[i])
local_survives = true;
} else {
is_extra[i] = deletable && !keep_is_file(keep, child_rel);
if (!is_extra[i])
local_survives = true;
}
free(child_rel);
}
/* Pass 1: extraneous subdirectories, descending. */
for (size_t i = 0; i < dir_count; i++) {
if (!is_extra[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false; operation_ok = false;
continue; if (!delete_walk_extra_files(dirfd, rel_path, entries, dir_count, count, ctx, is_extra,
} &local_survives))
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
bool child_all_removed = false;
if (childfd >= 0) {
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect,
deletable, &child_all_removed))
operation_ok = false; operation_ok = false;
close(childfd); if (!delete_walk_kept_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
} else if (errno != ENOENT) { shielded, &local_survives))
operation_ok = false; operation_ok = false;
}
if (child_all_removed && deletable) {
if (state->mode == DELETE_WALK_MODE_LIST) {
/* Record the directory with rsync's trailing slash. */
size_t len = strlen(child_rel);
char* copy = malloc(len + 2);
if (!copy) {
operation_ok = false;
} else {
memcpy(copy, child_rel, len);
copy[len] = '/';
copy[len + 1] = '\0';
if (!array_list_add(state->out, copy)) {
free(copy);
operation_ok = false;
} else {
(*state->recorded)++;
}
}
} else if (state->budget->deleted >= state->budget->max_delete) {
state->budget->limit_hit = true;
state->budget->skipped++;
local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, AT_REMOVEDIR) != 0) {
/* ENOENT: already gone (fine). ENOTEMPTY/EEXIST: the directory still
holds entries the walker leaves in place (a protected excluded
prefix, a kept file the manifest protects, a symlink); rsync leaves
such a directory behind, so this is not an error. Only genuine I/O
failures abort the deletion. */
if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST)
operation_ok = false;
local_survives = true;
} else {
state->budget->deleted++;
/* rsync reports a removed directory with a trailing slash. */
if (state->observer) {
size_t len = strlen(child_rel);
char* with_slash = malloc(len + 2);
if (with_slash) {
memcpy(with_slash, child_rel, len);
with_slash[len] = '/';
with_slash[len + 1] = '\0';
state->observer(state->observer_context, with_slash, DELETE_ENTRY_DIR);
free(with_slash);
} else {
state->observer(state->observer_context, child_rel, DELETE_ENTRY_DIR);
}
}
}
} else {
local_survives = true;
}
free(child_rel);
}
/* Pass 2: extraneous files, descending. */
for (size_t i = dir_count; i < count; i++) {
if (!is_extra[i])
continue;
if (state->mode == DELETE_WALK_MODE_LIST) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false;
continue;
}
char* copy = str_dup(child_rel);
if (!copy || !array_list_add(state->out, copy)) {
free(copy);
operation_ok = false;
} else {
(*state->recorded)++;
}
free(child_rel);
} else if (state->budget->deleted >= state->budget->max_delete) {
state->budget->limit_hit = true;
state->budget->skipped++;
local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, 0) != 0) {
if (errno != ENOENT)
operation_ok = false;
local_survives = true;
} else {
state->budget->deleted++;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (child_rel) {
if (state->observer)
state->observer(state->observer_context, child_rel,
delete_entry_type_of_mode(entries[i].mode));
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
}
free(child_rel);
}
}
/* Pass 3: kept subdirectories, ascending (rsync descends into these only
after the parent's own extras have been handled). */
for (size_t i = dir_count; i-- > 0;) {
if (is_extra[i] || shielded[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false;
continue;
}
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
bool child_all_removed = false;
if (childfd >= 0) {
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect,
deletable, &child_all_removed))
operation_ok = false;
close(childfd);
} else if (errno != ENOENT) {
operation_ok = false;
}
/* A kept/synchronized directory is never removed. */
local_survives = true;
free(child_rel);
}
free(shielded); free(shielded);
free(is_extra); free(is_extra);
@@ -504,8 +566,13 @@ bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
.recorded = &recorded, .recorded = &recorded,
.observer = NULL, .observer = NULL,
.observer_context = NULL}; .observer_context = NULL};
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count, DeleteWalkContext ctx = {.keep = &keep,
protect, false, &all_removed); .dirs = have_dirs ? &dirs : NULL,
.state = &state,
.skips = skips,
.skip_count = skip_count,
.protect = protect};
bool ok = delete_walk_fd(rootfd, "", &ctx, false, &all_removed);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
path_index_free(&keep); path_index_free(&keep);
@@ -557,8 +624,13 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
.recorded = NULL, .recorded = NULL,
.observer = observer, .observer = observer,
.observer_context = observer_context}; .observer_context = observer_context};
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count, DeleteWalkContext ctx = {.keep = &keep,
protect, false, &all_removed); .dirs = have_dirs ? &dirs : NULL,
.state = &state,
.skips = skips,
.skip_count = skip_count,
.protect = protect};
bool ok = delete_walk_fd(rootfd, "", &ctx, false, &all_removed);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
path_index_free(&keep); path_index_free(&keep);
+162 -135
View File
@@ -227,6 +227,166 @@ static void prefixed_delete_observer(void* context, const char* rel, DeleteEntry
--max-delete budget: once it is exhausted the remaining requests are skipped --max-delete budget: once it is exhausted the remaining requests are skipped
and counted. Returns false only on a genuine error (a confinement failure on and counted. Returns false only on a genuine error (a confinement failure on
a validated path or an I/O error), which fails the run. */ a validated path or an I/O error), which fails the run. */
/* How one missing-args request leaves the driver loop. The original walker
`continue`s past an invalid/protected/absent/budget-skipped request (without
breaking) but stops after a request that ran to completion while an error is
pending; NEXT/STOP preserve that control flow exactly. */
typedef enum { MISSING_ARG_NEXT, MISSING_ARG_STOP } MissingArgStep;
/* Remove a NON-empty missing-args directory recursively (--delete/--force in
effect): walk its contents through the budgeted extras walker so every removed
file/dir counts toward --max-delete (rsync parity), then remove the now-empty
directory itself, which costs one more budget unit. A run that hits the cap
leaves the remaining entries in place. The observer is wrapped so the nested
walk reports receive-root-relative paths. Sets the *removed and *ok outputs. */
static void delete_nonempty_missing_dir(const char* full, const char* rel,
DeleteBudgetState* budget, DeletePathObserver observer,
void* observer_context, bool* removed, bool* ok) {
ArrayList* no_keeps = array_list_create(free);
/* Never let an accounting slip (deleted > max_delete) underflow the remaining
budget into SIZE_MAX, which would grant unlimited deletions. */
size_t remaining =
budget->deleted >= budget->max_delete ? 0 : budget->max_delete - budget->deleted;
size_t contents_deleted = 0;
size_t contents_skipped = 0;
PrefixedDeleteObserver nested = {observer, observer_context, rel};
DeleteWalkResult walk =
no_keeps ? delete_extras_limited_observed(full, no_keeps, NULL, remaining, NULL, 0, NULL,
&contents_deleted, &contents_skipped,
observer ? prefixed_delete_observer : NULL,
observer ? &nested : NULL)
: DELETE_WALK_ERROR;
if (no_keeps)
array_list_delete(no_keeps);
budget->deleted += contents_deleted;
budget->skipped += contents_skipped;
if (walk == DELETE_WALK_LIMIT_REACHED) {
budget->limit_hit = true;
} else if (walk != DELETE_WALK_OK) {
*ok = false;
} else if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
} else if (file_remove_tree_secure(full)) {
/* The shared `if (removed)` tail charges this directory exactly once;
counting it here too would consume two budget units. */
*removed = true;
} else {
*ok = false;
}
}
/* Remove one missing-args destination mirror. `skips` holds the receiver
artifacts (staging directory, basis snapshots) that stay protected. Returns
MISSING_ARG_STOP when the driver loop must stop (a completed removal left a
genuine error pending) and MISSING_ARG_NEXT otherwise; *ok accumulates the
overall success across the whole run. */
static MissingArgStep delete_one_missing_arg(const Config* config, const char* rel,
const DeleteSkipSet* skips, DeleteBudgetState* budget,
DeletePathObserver observer, void* observer_context,
bool* ok) {
if (!rel || *rel == '\0' || *rel == '/' || has_path_traversal(rel)) {
/* Defensive only: receive_manifest_entries already validated every
section identically, so a controlled peer never reaches this branch. */
log_message(LOG_LEVEL_ERROR, "invalid missing-args delete path");
*ok = false;
return MISSING_ARG_NEXT;
}
bool at_root = strchr(rel, '/') == NULL;
if (path_under_skip_prefix(rel, at_root, skips->entries, skips->count)) {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args path '%s' is protected (staging directory or basis snapshot); "
"not deleting",
escaped ? escaped : "<allocation failed>");
free(escaped);
return MISSING_ARG_NEXT;
}
char* full = path_cat(config->receive_root_directory, rel);
if (!full) {
*ok = false;
return MISSING_ARG_NEXT;
}
char* leaf = NULL;
int parent_fd = file_open_secure_parent(full, &leaf, false);
if (parent_fd < 0) {
/* The mirror's parent directory may itself not exist on the destination
(a deeper missing entry whose leading directories were never created).
That is a no-op -- there is nothing to delete -- matching
file_remove_tree_secure's absent-path handling; only a genuine I/O
error (EACCES, a symlink loop, ...) fails the run. */
bool absent = errno == ENOENT || errno == ENOTDIR;
free(full);
free(leaf);
if (!absent)
*ok = false;
return MISSING_ARG_NEXT;
}
struct stat st;
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0) {
/* Already absent: nothing to delete (a no-op, not a deletion). */
if (errno != ENOENT)
*ok = false;
close(parent_fd);
free(leaf);
free(full);
return MISSING_ARG_NEXT;
}
/* An entry that exists is one deletion: skip it (and count it) when the
shared --max-delete budget is already exhausted. */
if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
close(parent_fd);
free(leaf);
free(full);
return MISSING_ARG_NEXT;
}
bool removed = false;
if (S_ISDIR(st.st_mode)) {
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) == 0) {
removed = true;
} else if (errno == ENOTEMPTY || errno == EEXIST) {
close(parent_fd);
parent_fd = -1;
free(leaf);
leaf = NULL;
if (config->use_delete || config->force_delete) {
delete_nonempty_missing_dir(full, rel, budget, observer, observer_context, &removed, ok);
} else {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args destination '%s' is a non-empty directory; use --force or "
"--delete to remove it",
escaped ? escaped : "<allocation failed>");
free(escaped);
}
} else if (errno != ENOENT) {
*ok = false;
}
} else {
if (unlinkat(parent_fd, leaf, 0) == 0) {
removed = true;
} else if (errno != ENOENT) {
*ok = false;
}
}
if (removed) {
budget->deleted++;
if (observer)
observer(observer_context, rel, delete_entry_type_of_mode(st.st_mode));
char* escaped = output_escape(rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
free(escaped);
}
if (parent_fd >= 0)
close(parent_fd);
free(leaf);
free(full);
return *ok ? MISSING_ARG_NEXT : MISSING_ARG_STOP;
}
static bool delete_missing_args_budgeted_observed(const Config* config, static bool delete_missing_args_budgeted_observed(const Config* config,
const DeleteManifest* manifest, const DeleteManifest* manifest,
DeleteBudgetState* budget, DeleteBudgetState* budget,
@@ -246,141 +406,8 @@ static bool delete_missing_args_budgeted_observed(const Config* config,
bool ok = true; bool ok = true;
for (int i = 0; i < manifest->missing->size; i++) { for (int i = 0; i < manifest->missing->size; i++) {
const char* rel = (const char*)manifest->missing->items[i]; const char* rel = (const char*)manifest->missing->items[i];
if (!rel || *rel == '\0' || *rel == '/' || has_path_traversal(rel)) { if (delete_one_missing_arg(config, rel, &skips, budget, observer, observer_context, &ok) ==
/* Defensive only: receive_manifest_entries already validated every MISSING_ARG_STOP)
section identically, so a controlled peer never reaches this branch. */
log_message(LOG_LEVEL_ERROR, "invalid missing-args delete path");
ok = false;
continue;
}
bool at_root = strchr(rel, '/') == NULL;
if (path_under_skip_prefix(rel, at_root, skips.entries, skips.count)) {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args path '%s' is protected (staging directory or basis snapshot); "
"not deleting",
escaped ? escaped : "<allocation failed>");
free(escaped);
continue;
}
char* full = path_cat(config->receive_root_directory, rel);
if (!full) {
ok = false;
continue;
}
char* leaf = NULL;
int parent_fd = file_open_secure_parent(full, &leaf, false);
if (parent_fd < 0) {
/* The mirror's parent directory may itself not exist on the destination
(a deeper missing entry whose leading directories were never created).
That is a no-op -- there is nothing to delete -- matching
file_remove_tree_secure's absent-path handling; only a genuine I/O
error (EACCES, a symlink loop, ...) fails the run. */
bool absent = errno == ENOENT || errno == ENOTDIR;
free(full);
free(leaf);
if (!absent)
ok = false;
continue;
}
struct stat st;
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0) {
/* Already absent: nothing to delete (a no-op, not a deletion). */
if (errno != ENOENT)
ok = false;
close(parent_fd);
free(leaf);
free(full);
continue;
}
/* An entry that exists is one deletion: skip it (and count it) when the
shared --max-delete budget is already exhausted. */
if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
close(parent_fd);
free(leaf);
free(full);
continue;
}
bool removed = false;
if (S_ISDIR(st.st_mode)) {
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) == 0) {
removed = true;
} else if (errno == ENOTEMPTY || errno == EEXIST) {
close(parent_fd);
parent_fd = -1;
free(leaf);
leaf = NULL;
if (config->use_delete || config->force_delete) {
/* Remove the contents entry-by-entry through the budgeted extras
walker so every deleted file/dir counts toward --max-delete (rsync
parity); the now-empty directory itself costs one more. A run that
hits the cap leaves the remaining entries in place. */
ArrayList* no_keeps = array_list_create(free);
/* Never let an accounting slip (deleted > max_delete) underflow the
remaining budget into SIZE_MAX, which would grant unlimited
deletions. */
size_t remaining =
budget->deleted >= budget->max_delete ? 0 : budget->max_delete - budget->deleted;
size_t contents_deleted = 0;
size_t contents_skipped = 0;
PrefixedDeleteObserver nested = {observer, observer_context, rel};
DeleteWalkResult walk =
no_keeps ? delete_extras_limited_observed(full, no_keeps, NULL, remaining, NULL, 0,
NULL, &contents_deleted, &contents_skipped,
observer ? prefixed_delete_observer : NULL,
observer ? &nested : NULL)
: DELETE_WALK_ERROR;
if (no_keeps)
array_list_delete(no_keeps);
budget->deleted += contents_deleted;
budget->skipped += contents_skipped;
if (walk == DELETE_WALK_LIMIT_REACHED) {
budget->limit_hit = true;
} else if (walk != DELETE_WALK_OK) {
ok = false;
} else if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
} else if (file_remove_tree_secure(full)) {
/* The shared `if (removed)` tail charges this directory exactly
once; counting it here too would consume two budget units. */
removed = true;
} else {
ok = false;
}
} else {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args destination '%s' is a non-empty directory; use --force or "
"--delete to remove it",
escaped ? escaped : "<allocation failed>");
free(escaped);
}
} else if (errno != ENOENT) {
ok = false;
}
} else {
if (unlinkat(parent_fd, leaf, 0) == 0) {
removed = true;
} else if (errno != ENOENT) {
ok = false;
}
}
if (removed) {
budget->deleted++;
if (observer)
observer(observer_context, rel, delete_entry_type_of_mode(st.st_mode));
char* escaped = output_escape(rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
free(escaped);
}
if (parent_fd >= 0)
close(parent_fd);
free(leaf);
free(full);
if (!ok)
break; break;
} }
delete_skips_free(&skips); delete_skips_free(&skips);