feat: exclude basis directories from --delete
Generalize the delete walker's protected-root-child skip into a prefix list. The receiver now passes both the --delay-updates staging directory and every basis-dir path, so a --delete run can never treat a basis snapshot (which a --link-dest run just linked from) as destination content to remove.
This commit is contained in:
+27
-12
@@ -204,9 +204,22 @@ static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* True when the relative path is, or lies below, one of the protected
|
||||||
|
prefixes. A prefix "a" therefore protects "a" and "a/b/c" but not "ab". */
|
||||||
|
static bool path_under_skip_prefix(const char* rel_path, const char* const* prefixes,
|
||||||
|
int prefix_count) {
|
||||||
|
for (int i = 0; i < prefix_count; i++) {
|
||||||
|
size_t prefix_len = strlen(prefixes[i]);
|
||||||
|
if (strncmp(rel_path, prefixes[i], prefix_len) == 0 &&
|
||||||
|
(rel_path[prefix_len] == '\0' || rel_path[prefix_len] == '/'))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest,
|
static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest,
|
||||||
size_t max_delete, size_t* deleted_count,
|
size_t max_delete, size_t* deleted_count,
|
||||||
const char* skip_root_child) {
|
const char* const* skip_prefixes, int skip_prefix_count) {
|
||||||
int scanfd = dup(dirfd);
|
int scanfd = dup(dirfd);
|
||||||
if (scanfd < 0)
|
if (scanfd < 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -220,18 +233,19 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
|
|||||||
while ((entry = readdir(dir)) != NULL) {
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
continue;
|
continue;
|
||||||
/* A --delay-updates run keeps its staging directory as a direct child of
|
|
||||||
the receive root. Its contents are not manifest entries yet (they are
|
|
||||||
published after deletion), so descending into it would delete every
|
|
||||||
staged file as an "extra". Skip only the top-level staging name; nested
|
|
||||||
directories with the same name are ordinary destination content. */
|
|
||||||
if (rel_path[0] == '\0' && skip_root_child && strcmp(entry->d_name, skip_root_child) == 0)
|
|
||||||
continue;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entry->d_name);
|
char* child_rel = path_cat((char*)rel_path, entry->d_name);
|
||||||
if (!child_rel) {
|
if (!child_rel) {
|
||||||
operation_ok = false;
|
operation_ok = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
/* A --delay-updates run keeps its staging directory below the receive
|
||||||
|
root, and basis-dir snapshots live there too. Their contents are not
|
||||||
|
manifest entries, so descending into them would delete every staged /
|
||||||
|
basis file as an "extra". */
|
||||||
|
if (path_under_skip_prefix(child_rel, skip_prefixes, skip_prefix_count)) {
|
||||||
|
free(child_rel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
||||||
if (errno != ENOENT)
|
if (errno != ENOENT)
|
||||||
@@ -249,7 +263,7 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
|
|||||||
bool child_removed = false;
|
bool child_removed = false;
|
||||||
if (childfd >= 0) {
|
if (childfd >= 0) {
|
||||||
child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count,
|
child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count,
|
||||||
skip_root_child);
|
skip_prefixes, skip_prefix_count);
|
||||||
if (!child_removed)
|
if (!child_removed)
|
||||||
operation_ok = false;
|
operation_ok = false;
|
||||||
close(childfd);
|
close(childfd);
|
||||||
@@ -301,7 +315,7 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
|
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
|
||||||
const char* skip_root_child) {
|
const char* const* skip_prefixes, int skip_prefix_count) {
|
||||||
if (!manifest)
|
if (!manifest)
|
||||||
return false;
|
return false;
|
||||||
int rootfd;
|
int rootfd;
|
||||||
@@ -318,14 +332,15 @@ bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t ma
|
|||||||
if (rootfd < 0)
|
if (rootfd < 0)
|
||||||
return false;
|
return false;
|
||||||
size_t deleted_count = 0;
|
size_t deleted_count = 0;
|
||||||
bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count, skip_root_child);
|
bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count, skip_prefixes,
|
||||||
|
skip_prefix_count);
|
||||||
if (close(rootfd) != 0)
|
if (close(rootfd) != 0)
|
||||||
ok = false;
|
ok = false;
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool delete_extras(const char* dest_root, ArrayList* manifest) {
|
bool delete_extras(const char* dest_root, ArrayList* manifest) {
|
||||||
return delete_extras_limited(dest_root, manifest, SIZE_MAX, NULL);
|
return delete_extras_limited(dest_root, manifest, SIZE_MAX, NULL, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_path_traversal(const char* path) {
|
bool has_path_traversal(const char* path) {
|
||||||
|
|||||||
+7
-5
@@ -10,12 +10,14 @@ char* output_escape(const char* string, bool eight_bit_output);
|
|||||||
char* path_cat(const char* path1, const char* path2);
|
char* path_cat(const char* path1, const char* path2);
|
||||||
bool glob_match(const char* pattern, const char* str);
|
bool glob_match(const char* pattern, const char* str);
|
||||||
bool delete_extras(const char* dest_root, ArrayList* manifest);
|
bool delete_extras(const char* dest_root, ArrayList* manifest);
|
||||||
/* Remove files/dirs under dest_root that are not listed in manifest. When
|
/* Remove files/dirs under dest_root that are not listed in manifest. The
|
||||||
skip_root_child is non-NULL, a direct child of dest_root with that exact
|
delete walker never descends into (and so never removes) an entry whose
|
||||||
name is left untouched (used to protect the --delay-updates staging
|
relative path equals one of the skip_prefixes or lies below one: used to
|
||||||
directory, which holds files that are still to be published). */
|
protect the --delay-updates staging directory (files still to be published)
|
||||||
|
and the --compare-dest/--copy-dest/--link-dest basis trees (snapshots the
|
||||||
|
transfer links from, never destination content). */
|
||||||
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
|
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
|
||||||
const char* skip_root_child);
|
const char* const* skip_prefixes, int skip_prefix_count);
|
||||||
bool utils_set_authorized_root(int fd, const char* canonical_path);
|
bool utils_set_authorized_root(int fd, const char* canonical_path);
|
||||||
/* The fd-only compatibility form is fail-closed for path-based operations;
|
/* The fd-only compatibility form is fail-closed for path-based operations;
|
||||||
* callers should use utils_set_authorized_root with the canonical identity. */
|
* callers should use utils_set_authorized_root with the canonical identity. */
|
||||||
|
|||||||
Reference in New Issue
Block a user