quality: refactors (#321-#326) + fake-super directories (#319) #329
+258
-186
@@ -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++) {
|
operation_ok = false;
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
if (!delete_walk_extra_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
|
||||||
if (!child_rel) {
|
&local_survives))
|
||||||
operation_ok = false;
|
operation_ok = false;
|
||||||
continue;
|
if (!delete_walk_extra_files(dirfd, rel_path, entries, dir_count, count, ctx, is_extra,
|
||||||
}
|
&local_survives))
|
||||||
/* A --delay-updates run keeps its staging directory as a direct child of
|
operation_ok = false;
|
||||||
the receive root, and basis-dir snapshots live below it too. Their
|
if (!delete_walk_kept_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
|
||||||
contents are not manifest entries, so descending into them would delete
|
shielded, &local_survives))
|
||||||
every staged / basis file as an "extra". Only the staging name (a
|
operation_ok = false;
|
||||||
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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user