feat(filter): per-directory merge rules at the receiver; implement e/n/w/- modifiers

This commit is contained in:
2026-09-23 22:46:14 +02:00
parent 29f8be161c
commit b414f197af
22 changed files with 1154 additions and 192 deletions
+14 -9
View File
File diff suppressed because one or more lines are too long
+21 -8
View File
@@ -330,9 +330,11 @@ int send_list_only(const Config* config) {
} }
/* Send the delete manifest to the server. Returns 0 on success, -1 on /* Send the delete manifest to the server. Returns 0 on success, -1 on
failure. It carries FOUR sections: the keep-set paths, the protected failure. It carries FOUR path sections (keep-set paths, protected excluded
excluded prefixes, the --delete-missing-args exact-delete paths, and the prefixes, --delete-missing-args exact-delete paths, and the destination-
destination-relative directories the sender synchronized this run. relative directories the sender synchronized this run) followed by the
protocol-2.30.0 per-directory filter-rule block (`per_dir_rules`, the rules
the scan compiled from each directory's merge files).
When --delete-excluded is given `protected` is empty: excluded destination When --delete-excluded is given `protected` is empty: excluded destination
mirrors are then ordinary extras and are removed. When mirrors are then ordinary extras and are removed. When
--delete-missing-args is active `missing_args` holds the destination mirrors --delete-missing-args is active `missing_args` holds the destination mirrors
@@ -346,7 +348,8 @@ int send_list_only(const Config* config) {
frame. A heavily filtered source whose exclusion list is large therefore frame. A heavily filtered source whose exclusion list is large therefore
fails the run cleanly on the receiver rather than being truncated. */ fails the run cleanly on the receiver rather than being truncated. */
int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefixes, int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefixes,
ArrayList* size_skipped, ArrayList* missing_args, ArrayList* synced_dirs) { ArrayList* size_skipped, ArrayList* missing_args, ArrayList* synced_dirs,
const FilterRuleList* per_dir_rules) {
if (!send_status(fd, STATUS_MANIFEST)) if (!send_status(fd, STATUS_MANIFEST))
return -1; return -1;
int keep_count = manifest ? manifest->size : 0; int keep_count = manifest ? manifest->size : 0;
@@ -389,6 +392,11 @@ int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefi
if (!send_wire_str(fd, (char*)synced_dirs->items[i])) if (!send_wire_str(fd, (char*)synced_dirs->items[i]))
return -1; return -1;
} }
/* Protocol 2.30.0: the receiver-side per-directory filter rules discovered by
the sender's scan, so the whole-tree commit walker can shield a
destination-only entry that matches only a per-directory merge rule. */
if (!delete_filter_dir_rules_send(fd, per_dir_rules))
return -1;
return 0; return 0;
} }
@@ -409,11 +417,11 @@ int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefi
bool send_delete_manifest_early(Client* client, ArrayList* manifest, ArrayList* protected_prefixes, bool send_delete_manifest_early(Client* client, ArrayList* manifest, ArrayList* protected_prefixes,
ArrayList* size_skipped, ArrayList* missing_args, ArrayList* size_skipped, ArrayList* missing_args,
ArrayList* synced_dirs) { ArrayList* synced_dirs, const FilterRuleList* per_dir_rules) {
if (!client || !manifest) if (!client || !manifest)
return false; return false;
if (send_delete_manifest(client->file_descriptor, manifest, protected_prefixes, size_skipped, if (send_delete_manifest(client->file_descriptor, manifest, protected_prefixes, size_skipped,
missing_args, synced_dirs) != 0) missing_args, synced_dirs, per_dir_rules) != 0)
return false; return false;
Status ack; Status ack;
/* The wait is long (up to an hour) and runs inline on this thread: a helper /* The wait is long (up to an hour) and runs inline on this thread: a helper
@@ -497,6 +505,7 @@ int send_dry_run_remote(Config* config) {
ArrayList* dry_dirs = NULL; ArrayList* dry_dirs = NULL;
ArrayList* dry_excluded = NULL; ArrayList* dry_excluded = NULL;
ArrayList* dry_size_skipped = NULL; ArrayList* dry_size_skipped = NULL;
FilterRuleList* dry_per_dir = NULL;
if (!config_send(client->file_descriptor, config)) if (!config_send(client->file_descriptor, config))
goto dry_fail; goto dry_fail;
receive_daemon_motd(client, config); receive_daemon_motd(client, config);
@@ -509,8 +518,10 @@ int send_dry_run_remote(Config* config) {
dry_manifest = array_list_create(free); dry_manifest = array_list_create(free);
dry_dirs = array_list_create(free); dry_dirs = array_list_create(free);
dry_size_skipped = array_list_create(free); dry_size_skipped = array_list_create(free);
if (!dry_manifest || !dry_dirs || !dry_size_skipped) dry_per_dir = filter_rule_list_create();
if (!dry_manifest || !dry_dirs || !dry_size_skipped || !dry_per_dir)
goto dry_fail; goto dry_fail;
prepared.options.per_dir_rules = dry_per_dir;
if (!config->delete_excluded) { if (!config->delete_excluded) {
dry_excluded = array_list_create(free); dry_excluded = array_list_create(free);
if (!dry_excluded) if (!dry_excluded)
@@ -612,7 +623,7 @@ int send_dry_run_remote(Config* config) {
bool early_delete = config->use_delete && config_delete_timing_early(config); bool early_delete = config->use_delete && config_delete_timing_early(config);
if (dry_manifest) { if (dry_manifest) {
if (send_delete_manifest(client->file_descriptor, dry_manifest, dry_excluded, dry_size_skipped, if (send_delete_manifest(client->file_descriptor, dry_manifest, dry_excluded, dry_size_skipped,
NULL, dry_dirs) != 0) NULL, dry_dirs, dry_per_dir) != 0)
goto dry_fail; goto dry_fail;
if (early_delete) { if (early_delete) {
Status ack; Status ack;
@@ -672,6 +683,8 @@ dry_fail:
array_list_delete(dry_excluded); array_list_delete(dry_excluded);
if (dry_size_skipped) if (dry_size_skipped)
array_list_delete(dry_size_skipped); array_list_delete(dry_size_skipped);
if (dry_per_dir)
filter_rule_list_free(dry_per_dir);
if (scanner) if (scanner)
directory_scanner_destroy(scanner); directory_scanner_destroy(scanner);
prepared_scanner_destroy(&prepared); prepared_scanner_destroy(&prepared);
+25 -11
View File
@@ -1033,7 +1033,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
and wait for the receiver to delete extras before streaming any data. */ and wait for the receiver to delete extras before streaming any data. */
if (!send_delete_manifest_early(client, context->manifest, context->excluded_paths, if (!send_delete_manifest_early(client, context->manifest, context->excluded_paths,
context->size_skipped_paths, context->missing_args, context->size_skipped_paths, context->missing_args,
context->synced_dirs)) { context->synced_dirs, context->per_dir_rules)) {
pipeline_cancel(context); pipeline_cancel(context);
disconnect_transfer_client(client); disconnect_transfer_client(client);
mark_sender_done(context); mark_sender_done(context);
@@ -1163,7 +1163,8 @@ static int send_chunks_multithreaded(void* pipeline_context) {
log_message(LOG_LEVEL_WARNING, "IO error encountered -- skipping file deletion"); log_message(LOG_LEVEL_WARNING, "IO error encountered -- skipping file deletion");
} else if (send_delete_manifest(client->file_descriptor, context->manifest, } else if (send_delete_manifest(client->file_descriptor, context->manifest,
context->excluded_paths, context->size_skipped_paths, context->excluded_paths, context->size_skipped_paths,
context->missing_args, context->synced_dirs) != 0) { context->missing_args, context->synced_dirs,
context->per_dir_rules) != 0) {
goto send_fail; goto send_fail;
} }
} else if (context->config->delete_missing_args && !context->early_delete && } else if (context->config->delete_missing_args && !context->early_delete &&
@@ -1171,7 +1172,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
/* --delete-missing-args without --delete: no keep-set is built, but the /* --delete-missing-args without --delete: no keep-set is built, but the
exact-delete paths still ride the same manifest frame (commit once the exact-delete paths still ride the same manifest frame (commit once the
transfer succeeded). */ transfer succeeded). */
if (send_delete_manifest(client->file_descriptor, NULL, NULL, NULL, context->missing_args, if (send_delete_manifest(client->file_descriptor, NULL, NULL, NULL, context->missing_args, NULL,
NULL) != 0) NULL) != 0)
goto send_fail; goto send_fail;
} }
@@ -1285,6 +1286,7 @@ static int scan_directory_multithreaded(void* pipeline_context) {
its protected lists, so the data pass must not append to them again. */ its protected lists, so the data pass must not append to them again. */
if (!context->early_delete && !context->delete_plans) { if (!context->early_delete && !context->delete_plans) {
prepared.options.excluded_paths = context->excluded_paths; prepared.options.excluded_paths = context->excluded_paths;
prepared.options.per_dir_rules = context->per_dir_rules;
/* The root marker for a full recursive transfer is already in the list; do /* The root marker for a full recursive transfer is already in the list; do
not let the scanner append every directory to it. */ not let the scanner append every directory to it. */
if (context->config->files_from_set != NULL) if (context->config->files_from_set != NULL)
@@ -1518,6 +1520,8 @@ typedef struct {
ArrayList* synced_dirs; ArrayList* synced_dirs;
ArrayList* plan_dirs; ArrayList* plan_dirs;
ArrayList* missing_args; ArrayList* missing_args;
/* Per-directory filter rules compiled by the scan (protocol 2.30.0). */
FilterRuleList* per_dir_rules;
PreparedScanner prepared; PreparedScanner prepared;
StopCondition stop; StopCondition stop;
TransferStats transfer_stats; TransferStats transfer_stats;
@@ -1562,9 +1566,11 @@ static bool send_files_prepare(Config* config, SendFilesState* state) {
} }
state->size_skipped = array_list_create(free); state->size_skipped = array_list_create(free);
state->synced_dirs = array_list_create(free); state->synced_dirs = array_list_create(free);
if (!state->size_skipped || !state->synced_dirs) state->per_dir_rules = filter_rule_list_create();
if (!state->size_skipped || !state->synced_dirs || !state->per_dir_rules)
return false; return false;
state->prepared.options.size_skipped_paths = state->size_skipped; state->prepared.options.size_skipped_paths = state->size_skipped;
state->prepared.options.per_dir_rules = state->per_dir_rules;
/* Only a --files-from subset confines the extras walk to the directories /* Only a --files-from subset confines the extras walk to the directories
the scan synchronized; a full recursive transfer deletes throughout the the scan synchronized; a full recursive transfer deletes throughout the
receive root, so mark the root itself (the "." sentinel) and let the receive root, so mark the root itself (the "." sentinel) and let the
@@ -1629,9 +1635,9 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
log_message(LOG_LEVEL_WARNING, "IO error encountered -- skipping file deletion"); log_message(LOG_LEVEL_WARNING, "IO error encountered -- skipping file deletion");
skip_delete = true; skip_delete = true;
} else { } else {
early_ok = early_ok = send_delete_manifest_early(client, early_manifest, state->excluded,
send_delete_manifest_early(client, early_manifest, state->excluded, state->size_skipped, state->size_skipped, state->missing_args,
state->missing_args, state->synced_dirs); state->synced_dirs, state->per_dir_rules);
} }
} }
array_list_delete(early_manifest); array_list_delete(early_manifest);
@@ -1640,6 +1646,7 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
state->prepared.options.excluded_paths = NULL; state->prepared.options.excluded_paths = NULL;
state->prepared.options.size_skipped_paths = NULL; state->prepared.options.size_skipped_paths = NULL;
state->prepared.options.synced_dirs = NULL; state->prepared.options.synced_dirs = NULL;
state->prepared.options.per_dir_rules = NULL;
if (!prescan_ok || (!early_ok && !skip_delete)) { if (!prescan_ok || (!early_ok && !skip_delete)) {
array_list_delete(prescan_chunks); array_list_delete(prescan_chunks);
return false; return false;
@@ -1669,7 +1676,7 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
config->files_from_set ? state->synced_dirs : (walk_root ? state->synced_dirs : NULL); config->files_from_set ? state->synced_dirs : (walk_root ? state->synced_dirs : NULL);
delete_plan_sender_finalize(state->plan_sender, scope, walk_root); delete_plan_sender_finalize(state->plan_sender, scope, walk_root);
delete_plan_sender_set_config(state->plan_sender, state->excluded, state->size_skipped, delete_plan_sender_set_config(state->plan_sender, state->excluded, state->size_skipped,
state->missing_args); state->missing_args, state->per_dir_rules);
if (state->had_scan_io && delete_plan_sender_empty(state->plan_sender)) { if (state->had_scan_io && delete_plan_sender_empty(state->plan_sender)) {
log_message(LOG_LEVEL_ERROR, log_message(LOG_LEVEL_ERROR,
"source scan hit an I/O error before finding any file; refusing to delete " "source scan hit an I/O error before finding any file; refusing to delete "
@@ -1693,6 +1700,7 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
state->prepared.options.size_skipped_paths = NULL; state->prepared.options.size_skipped_paths = NULL;
state->prepared.options.synced_dirs = NULL; state->prepared.options.synced_dirs = NULL;
state->prepared.options.plan_dirs = NULL; state->prepared.options.plan_dirs = NULL;
state->prepared.options.per_dir_rules = NULL;
if (!prescan_ok || (!plans_ok && !skip_delete)) if (!prescan_ok || (!plans_ok && !skip_delete))
return false; return false;
} else if (config->use_delete) { } else if (config->use_delete) {
@@ -1874,7 +1882,8 @@ static int send_files_finalize(const Config* config, SendFilesState* state) {
modes the deletion already went out with the data, so nothing is modes the deletion already went out with the data, so nothing is
re-sent here. */ re-sent here. */
if (send_delete_manifest(client->file_descriptor, state->manifest, state->excluded, if (send_delete_manifest(client->file_descriptor, state->manifest, state->excluded,
state->size_skipped, state->missing_args, state->synced_dirs) != 0) { state->size_skipped, state->missing_args, state->synced_dirs,
state->per_dir_rules) != 0) {
if (state->manifest) { if (state->manifest) {
array_list_delete(state->manifest); array_list_delete(state->manifest);
state->manifest = NULL; state->manifest = NULL;
@@ -1949,6 +1958,8 @@ static void send_files_cleanup(SendFilesState* state) {
array_list_delete(state->plan_dirs); array_list_delete(state->plan_dirs);
if (state->missing_args) if (state->missing_args)
array_list_delete(state->missing_args); array_list_delete(state->missing_args);
if (state->per_dir_rules)
filter_rule_list_free(state->per_dir_rules);
if (state->remove_sources) if (state->remove_sources)
array_list_delete(state->remove_sources); array_list_delete(state->remove_sources);
if (state->dir_entries) if (state->dir_entries)
@@ -2130,7 +2141,8 @@ static int send_files_multithreaded_impl(Config* config) {
confined; only a --files-from subset records concrete directories. */ confined; only a --files-from subset records concrete directories. */
context->size_skipped_paths = array_list_create(free); context->size_skipped_paths = array_list_create(free);
context->synced_dirs = array_list_create(free); context->synced_dirs = array_list_create(free);
if (!context->size_skipped_paths || !context->synced_dirs) { context->per_dir_rules = filter_rule_list_create();
if (!context->size_skipped_paths || !context->synced_dirs || !context->per_dir_rules) {
pipeline_context_sender_destroy(context); pipeline_context_sender_destroy(context);
return 1; return 1;
} }
@@ -2159,6 +2171,7 @@ static int send_files_multithreaded_impl(Config* config) {
if (context->excluded_paths) if (context->excluded_paths)
prepared.options.excluded_paths = context->excluded_paths; prepared.options.excluded_paths = context->excluded_paths;
prepared.options.size_skipped_paths = context->size_skipped_paths; prepared.options.size_skipped_paths = context->size_skipped_paths;
prepared.options.per_dir_rules = context->per_dir_rules;
/* The root marker for a full recursive transfer is already in the list; /* The root marker for a full recursive transfer is already in the list;
only a --files-from subset needs the scanner to record directories. */ only a --files-from subset needs the scanner to record directories. */
if (config->files_from_set != NULL) if (config->files_from_set != NULL)
@@ -2199,7 +2212,8 @@ static int send_files_multithreaded_impl(Config* config) {
: (walk_root ? context->synced_dirs : NULL); : (walk_root ? context->synced_dirs : NULL);
delete_plan_sender_finalize(context->delete_plans, scope, walk_root); delete_plan_sender_finalize(context->delete_plans, scope, walk_root);
delete_plan_sender_set_config(context->delete_plans, context->excluded_paths, delete_plan_sender_set_config(context->delete_plans, context->excluded_paths,
context->size_skipped_paths, context->missing_args); context->size_skipped_paths, context->missing_args,
context->per_dir_rules);
} }
bool empty = per_dir bool empty = per_dir
? (context->delete_plans && delete_plan_sender_empty(context->delete_plans)) ? (context->delete_plans && delete_plan_sender_empty(context->delete_plans))
+3 -2
View File
@@ -102,9 +102,10 @@ int send_dry_run_manifest(const Config* config);
int send_list_only(const Config* config); int send_list_only(const Config* config);
int send_dry_run_remote(Config* config); int send_dry_run_remote(Config* config);
int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefixes, int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefixes,
ArrayList* size_skipped, ArrayList* missing_args, ArrayList* synced_dirs); ArrayList* size_skipped, ArrayList* missing_args, ArrayList* synced_dirs,
const FilterRuleList* per_dir_rules);
bool send_delete_manifest_early(Client* client, ArrayList* manifest, ArrayList* protected_prefixes, bool send_delete_manifest_early(Client* client, ArrayList* manifest, ArrayList* protected_prefixes,
ArrayList* size_skipped, ArrayList* missing_args, ArrayList* size_skipped, ArrayList* missing_args,
ArrayList* synced_dirs); ArrayList* synced_dirs, const FilterRuleList* per_dir_rules);
#endif #endif
+7
View File
@@ -115,6 +115,13 @@ typedef struct {
* directories, exactly like rsync; the receive root is the "." sentinel. * directories, exactly like rsync; the receive root is the "." sentinel.
* Guarded by `excluded_mutex`. */ * Guarded by `excluded_mutex`. */
ArrayList* synced_dirs; ArrayList* synced_dirs;
/* Per-directory filter-rule sink (optional): when non-NULL the scanner appends
* a deep copy of every rule it reads from a per-directory merge file, each
* carrying its owner directory and no-inherit flag (see filter.h). The delete
* carriers transmit them so the receiver re-derives the per-directory
* protect/risk set for destination-only entries. Guarded by `excluded_mutex`
* like the other sinks. */
FilterRuleList* per_dir_rules;
/* Delete-plan directory sink (optional): when non-NULL the scanner appends /* Delete-plan directory sink (optional): when non-NULL the scanner appends
* the destination-relative path of every directory it traverses (except the * the destination-relative path of every directory it traverses (except the
* receive root). The per-directory --delete-during/--delete-delay plan * receive root). The per-directory --delete-during/--delete-delay plan
+19 -1
View File
@@ -511,13 +511,31 @@ FilterRuleList* read_dir_filters(const ScannerOptions* options, const char* dir_
} }
if (base) { if (base) {
for (int i = 0; i < base->dir_merge_count; i++) { for (int i = 0; i < base->dir_merge_count; i++) {
if (!filter_file_append(own, dir_path, base->dir_merge_names[i], rel, &opts, &exists, err, if (!filter_dir_merge_append(own, dir_path, &base->dir_merges[i], rel, &opts, &exists, err,
err_size)) err_size))
goto fail; goto fail;
if (exists && any_exists) if (exists && any_exists)
*any_exists = true; *any_exists = true;
} }
} }
/* Mirror the directory's rules into the delete-carrier sink so the receiver
* can reconstruct its per-directory protect/risk set. */
if (options->per_dir_rules && own->count > 0) {
mtx_t* mtx = options->excluded_mutex;
if (mtx)
mtx_lock(mtx);
for (int i = 0; i < own->count; i++) {
FilterRule* copy = filter_rule_clone(own->items[i]);
if (!copy || !filter_rule_list_add(options->per_dir_rules, copy)) {
filter_rule_free(copy);
if (mtx)
mtx_unlock(mtx);
goto fail;
}
}
if (mtx)
mtx_unlock(mtx);
}
return own; return own;
fail: fail:
filter_rule_list_free(own); filter_rule_list_free(own);
+24 -13
View File
@@ -32,6 +32,19 @@ static bool keep_is_file(const PathIndex* index, const char* rel_path) {
return path_index_contains(index, rel_path); return path_index_contains(index, rel_path);
} }
/* rsync's receiver-side verdict for one candidate extra: the per-directory
* chain first (deepest directory before ancestors), then the command-line base
* rules. Either rule set may be absent. */
FilterAction delete_protect_verdict(const DeleteProtectRules* protect, const char* rel_path,
const char* leaf, bool is_dir) {
if (!protect)
return FILTER_ACTION_NONE;
FilterAction action = filter_dir_rules_apply_side(protect->dir_rules, rel_path, leaf, is_dir);
if (action != FILTER_ACTION_NONE)
return action;
return filter_rules_apply_side(protect->base_rules, rel_path, leaf, is_dir, FILTER_SIDE_RECEIVER);
}
/* Classify a removed entry from its st_mode for the per-type delete counters. */ /* Classify a removed entry from its st_mode for the per-type delete counters. */
DeleteEntryType delete_entry_type_of_mode(mode_t mode) { DeleteEntryType delete_entry_type_of_mode(mode_t mode) {
if (S_ISDIR(mode)) if (S_ISDIR(mode))
@@ -209,7 +222,7 @@ typedef struct {
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 PathIndex* keep,
const PathIndex* dirs, DeleteWalkState* state, const PathIndex* dirs, DeleteWalkState* state,
const DeleteSkipEntry* skips, int skip_count, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, bool parent_deletable, const DeleteProtectRules* protect, bool parent_deletable,
bool* all_removed) { bool* all_removed) {
DeleteDirEntry* entries = NULL; DeleteDirEntry* entries = NULL;
size_t count = 0; size_t count = 0;
@@ -260,9 +273,8 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) { if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) {
shielded[i] = true; shielded[i] = true;
local_survives = true; local_survives = true;
} else if (protect_rules && } else if (delete_protect_verdict(protect, child_rel, entries[i].name, entries[i].is_dir) ==
filter_rules_apply_side(protect_rules, child_rel, entries[i].name, entries[i].is_dir, FILTER_ACTION_PROTECT) {
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT) {
/* A first-match protect rule shields the extra; for a directory the whole /* A first-match protect rule shields the extra; for a directory the whole
subtree is shielded (rsync prunes an excluded directory), so do not subtree is shielded (rsync prunes an excluded directory), so do not
descend. */ descend. */
@@ -293,7 +305,7 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
bool child_all_removed = false; bool child_all_removed = false;
if (childfd >= 0) { if (childfd >= 0) {
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect_rules, if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect,
deletable, &child_all_removed)) deletable, &child_all_removed))
operation_ok = false; operation_ok = false;
close(childfd); close(childfd);
@@ -408,7 +420,7 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
bool child_all_removed = false; bool child_all_removed = false;
if (childfd >= 0) { if (childfd >= 0) {
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect_rules, if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect,
deletable, &child_all_removed)) deletable, &child_all_removed))
operation_ok = false; operation_ok = false;
close(childfd); close(childfd);
@@ -443,7 +455,7 @@ static int open_destination_root(const char* dest_root) {
bool delete_extras_list(const char* dest_root, const ArrayList* manifest, bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count, const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, ArrayList* out, size_t* count_out) { const DeleteProtectRules* protect, ArrayList* out, size_t* count_out) {
if (count_out) if (count_out)
*count_out = 0; *count_out = 0;
if (!manifest || !out) if (!manifest || !out)
@@ -474,7 +486,7 @@ bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
.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, bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count,
protect_rules, false, &all_removed); protect, false, &all_removed);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
path_index_free(&keep); path_index_free(&keep);
@@ -488,7 +500,7 @@ bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest, DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, size_t max_delete, const ArrayList* synced_dirs, size_t max_delete,
const DeleteSkipEntry* skips, int skip_count, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, const DeleteProtectRules* protect,
size_t* deleted_out, size_t* skipped_out, size_t* deleted_out, size_t* skipped_out,
DeletePathObserver observer, DeletePathObserver observer,
void* observer_context) { void* observer_context) {
@@ -527,7 +539,7 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
.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, bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count,
protect_rules, false, &all_removed); protect, false, &all_removed);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
path_index_free(&keep); path_index_free(&keep);
@@ -545,11 +557,10 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest, DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, size_t max_delete, const ArrayList* synced_dirs, size_t max_delete,
const DeleteSkipEntry* skips, int skip_count, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, size_t* deleted_out, const DeleteProtectRules* protect, size_t* deleted_out,
size_t* skipped_out) { size_t* skipped_out) {
return delete_extras_limited_observed(dest_root, manifest, synced_dirs, max_delete, skips, return delete_extras_limited_observed(dest_root, manifest, synced_dirs, max_delete, skips,
skip_count, protect_rules, deleted_out, skipped_out, NULL, skip_count, protect, deleted_out, skipped_out, NULL, NULL);
NULL);
} }
bool delete_extras(const char* dest_root, const ArrayList* manifest) { bool delete_extras(const char* dest_root, const ArrayList* manifest) {
+23 -4
View File
@@ -3,6 +3,7 @@
#include "array_list.h" #include "array_list.h"
#include "config.h" #include "config.h"
#include "filter.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <sys/stat.h> #include <sys/stat.h>
@@ -29,6 +30,24 @@ typedef enum {
DELETE_WALK_ERROR DELETE_WALK_ERROR
} DeleteWalkResult; } DeleteWalkResult;
/* Receiver-side delete-protection rules for one walk. `base_rules` is the
* command-line rule set the config frame carried (owner "" rules); `dir_rules`
* is the received per-directory rule set (rules carrying their owner directory
* and no-inherit flag). Either may be NULL. */
typedef struct {
const FilterRuleList* base_rules;
const FilterRuleList* dir_rules;
} DeleteProtectRules;
/* rsync's first-match-wins receiver verdict for one candidate extra: the
* per-directory chain is evaluated first (the containing directory's rules,
* then each ancestor's, then the receive root's), then the base rules. Returns
* FILTER_ACTION_PROTECT when the entry is shielded by a receiver-side exclude,
* FILTER_ACTION_RISK when an include explicitly leaves it at risk, or
* FILTER_ACTION_NONE when no rule matched. */
FilterAction delete_protect_verdict(const DeleteProtectRules* protect, const char* rel_path,
const char* leaf, bool is_dir);
/* One protected entry for the delete walker. When top_level_only is true the /* One protected entry for the delete walker. When top_level_only is true the
prefix is skipped only as a DIRECT child of dest_root (the --delay-updates prefix is skipped only as a DIRECT child of dest_root (the --delay-updates
staging directory, which must not hide genuine extras inside a nested staging directory, which must not hide genuine extras inside a nested
@@ -100,7 +119,7 @@ int delete_dir_entry_cmp_asc(const void* a, const void* b);
DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest, DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, size_t max_delete, const ArrayList* synced_dirs, size_t max_delete,
const DeleteSkipEntry* skips, int skip_count, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, size_t* deleted_out, const DeleteProtectRules* protect, size_t* deleted_out,
size_t* skipped_out); size_t* skipped_out);
/* Entry kind of a removed path, reported to the delete observer so the receiver /* Entry kind of a removed path, reported to the delete observer so the receiver
@@ -124,14 +143,14 @@ DeleteEntryType delete_entry_type_of_mode(mode_t mode);
/* `delete_extras_limited_observed` is delete_extras_limited with an optional /* `delete_extras_limited_observed` is delete_extras_limited with an optional
* observer; the observer is invoked only for entries truly removed. When * observer; the observer is invoked only for entries truly removed. When
* `protect_rules` is non-NULL its receiver-side verdict is evaluated for every * `protect` is non-NULL its receiver-side verdict is evaluated for every
* candidate extra: a first-match PROTECT leaves the entry (and, for a * candidate extra: a first-match PROTECT leaves the entry (and, for a
* directory, its whole subtree) in place, while RISK/NONE fall through to the * directory, its whole subtree) in place, while RISK/NONE fall through to the
* ordinary skip-prefix/keep-set logic. */ * ordinary skip-prefix/keep-set logic. */
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest, DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, size_t max_delete, const ArrayList* synced_dirs, size_t max_delete,
const DeleteSkipEntry* skips, int skip_count, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, const DeleteProtectRules* protect,
size_t* deleted_out, size_t* skipped_out, size_t* deleted_out, size_t* skipped_out,
DeletePathObserver observer, DeletePathObserver observer,
void* observer_context); void* observer_context);
@@ -142,7 +161,7 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
strings appended to `out` and receives their count in *count_out. */ strings appended to `out` and receives their count in *count_out. */
bool delete_extras_list(const char* dest_root, const ArrayList* manifest, bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count, const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, ArrayList* out, size_t* count_out); const DeleteProtectRules* protect, ArrayList* out, size_t* count_out);
bool delete_extras(const char* dest_root, const ArrayList* manifest); bool delete_extras(const char* dest_root, const ArrayList* manifest);
/* Build the delete walk's skip-prefix set from the config's --delay-updates /* Build the delete walk's skip-prefix set from the config's --delay-updates
+15 -2
View File
@@ -19,6 +19,7 @@
#include "data.h" #include "data.h"
#include "delay_updates.h" #include "delay_updates.h"
#include "delete_commit.h" #include "delete_commit.h"
#include "delete_plan.h"
#include "delta.h" #include "delta.h"
#include "file.h" #include "file.h"
#include "format.h" #include "format.h"
@@ -102,6 +103,13 @@ DeleteManifest* receive_manifest_entries(int fd) {
delete_manifest_free(manifest); delete_manifest_free(manifest);
return NULL; return NULL;
} }
/* Per-directory filter rules (protocol 2.30.0) follow the manifest sections
* with their own bounded self-describing format. */
if (!delete_filter_dir_rules_receive(fd, &manifest->per_dir_rules)) {
delete_manifest_free(manifest);
send_status(fd, STATUS_ERROR);
return NULL;
}
return manifest; return manifest;
} }
@@ -112,6 +120,7 @@ void delete_manifest_free(DeleteManifest* manifest) {
array_list_delete(manifest->protected); array_list_delete(manifest->protected);
array_list_delete(manifest->missing); array_list_delete(manifest->missing);
array_list_delete(manifest->dirs); array_list_delete(manifest->dirs);
filter_rule_list_free(manifest->per_dir_rules);
free(manifest); free(manifest);
} }
@@ -160,9 +169,11 @@ static bool delete_extras_budgeted_observed(const Config* config, const DeleteMa
remaining = budget->max_delete - budget->deleted; remaining = budget->max_delete - budget->deleted;
size_t deleted = 0; size_t deleted = 0;
size_t skipped = 0; size_t skipped = 0;
DeleteProtectRules protect = {.base_rules = config->protect_rules,
.dir_rules = manifest->per_dir_rules};
DeleteWalkResult result = delete_extras_limited_observed( DeleteWalkResult result = delete_extras_limited_observed(
config->receive_root_directory, manifest->keeps, manifest->dirs, remaining, skips.entries, config->receive_root_directory, manifest->keeps, manifest->dirs, remaining, skips.entries,
skips.count, config->protect_rules, &deleted, &skipped, observer, observer_context); skips.count, &protect, &deleted, &skipped, observer, observer_context);
delete_skips_free(&skips); delete_skips_free(&skips);
budget->deleted += deleted; budget->deleted += deleted;
budget->skipped += skipped; budget->skipped += skipped;
@@ -386,8 +397,10 @@ bool manifest_would_delete_list(const Config* config, const DeleteManifest* mani
DeleteSkipSet skips; DeleteSkipSet skips;
if (!delete_skips_build(config, manifest->protected, NULL, true, &skips)) if (!delete_skips_build(config, manifest->protected, NULL, true, &skips))
return false; return false;
DeleteProtectRules protect = {.base_rules = config->protect_rules,
.dir_rules = manifest->per_dir_rules};
bool ok = delete_extras_list(config->receive_root_directory, manifest->keeps, manifest->dirs, bool ok = delete_extras_list(config->receive_root_directory, manifest->keeps, manifest->dirs,
skips.entries, skips.count, config->protect_rules, out, count_out); skips.entries, skips.count, &protect, out, count_out);
delete_skips_free(&skips); delete_skips_free(&skips);
return ok; return ok;
} }
+8
View File
@@ -4,6 +4,7 @@
#include "array_list.h" #include "array_list.h"
#include "config.h" #include "config.h"
#include "delete.h" #include "delete.h"
#include "filter.h"
#include <stdbool.h> #include <stdbool.h>
/* Delete-commit module: delete-manifest receive plus the budgeted extras and /* Delete-commit module: delete-manifest receive plus the budgeted extras and
@@ -30,6 +31,13 @@ typedef struct DeleteManifest {
leave untransmitted directories and the unlisted parts of listed ones leave untransmitted directories and the unlisted parts of listed ones
alone, matching rsync's "delete only in synchronized directories". */ alone, matching rsync's "delete only in synchronized directories". */
ArrayList* dirs; ArrayList* dirs;
/* Per-directory filter rules the sender compiled while scanning (protocol
2.30.0), each carrying its owner directory and no-inherit flag. The
receiver evaluates them (deepest before ancestors, then the command-line
base rules) against every candidate extra so a destination-only entry that
matches ONLY a per-directory `.rsync-filter`/dir-merge rule is shielded.
NULL when the sender transmitted none. */
FilterRuleList* per_dir_rules;
} DeleteManifest; } DeleteManifest;
void delete_manifest_free(DeleteManifest* manifest); void delete_manifest_free(DeleteManifest* manifest);
+177 -12
View File
@@ -48,6 +48,7 @@ struct DeletePlanSender {
const ArrayList* protected_prefixes; const ArrayList* protected_prefixes;
const ArrayList* size_skipped; const ArrayList* size_skipped;
const ArrayList* missing_args; const ArrayList* missing_args;
const FilterRuleList* per_dir_rules;
size_t entries; size_t entries;
/* Transmitted FILE entries only. The caller's "empty scan" safety guard keys /* Transmitted FILE entries only. The caller's "empty scan" safety guard keys
off this (an I/O error that hid every file must refuse to delete even when off this (an I/O error that hid every file must refuse to delete even when
@@ -284,12 +285,14 @@ bool delete_plan_sender_empty(const DeletePlanSender* sender) {
} }
void delete_plan_sender_set_config(DeletePlanSender* sender, const ArrayList* protected_prefixes, void delete_plan_sender_set_config(DeletePlanSender* sender, const ArrayList* protected_prefixes,
const ArrayList* size_skipped, const ArrayList* missing_args) { const ArrayList* size_skipped, const ArrayList* missing_args,
const FilterRuleList* per_dir_rules) {
if (!sender) if (!sender)
return; return;
sender->protected_prefixes = protected_prefixes; sender->protected_prefixes = protected_prefixes;
sender->size_skipped = size_skipped; sender->size_skipped = size_skipped;
sender->missing_args = missing_args; sender->missing_args = missing_args;
sender->per_dir_rules = per_dir_rules;
} }
/* True when `dir` is `root` itself or a descendant of it (path-component /* True when `dir` is `root` itself or a descendant of it (path-component
@@ -320,6 +323,160 @@ static int send_str_section(int fd, const ArrayList* list) {
return 0; return 0;
} }
/* The directory a rule belongs to (its owner, or the transfer root for ""). */
static const char* filter_dir_rule_owner(const FilterRule* rule) {
return (rule && rule->owner) ? rule->owner : "";
}
/* Transmit the received-side per-directory filter rules (protocol 2.30.0) as a
* self-describing list of directory groups: a group count, then for each group
* the relative owner directory followed by that directory's rule records (run
* order = the sender's traversal/rule order). Rules of one directory are
* appended to the sink contiguously, so runs reproduce the compilation order.
* Bounded by MAX_FILTER_RULES / MAX_FILTER_BYTES and MAX_PROTECT_PATTERN_LEN so
* the peer never sees a frame it would reject. */
bool delete_filter_dir_rules_send(int fd, const FilterRuleList* rules) {
int count = rules ? rules->count : 0;
if (count < 0 || count > MAX_FILTER_RULES) {
log_message(LOG_LEVEL_ERROR, "too many per-directory filter rules: %d (maximum %d)", count,
MAX_FILTER_RULES);
return false;
}
for (int i = 0; i < count; i++) {
const FilterRule* rule = rules->items[i];
size_t pattern_len = rule && rule->pattern ? strlen(rule->pattern) : 0;
if (!rule || !rule->pattern || pattern_len == 0 || pattern_len > MAX_PROTECT_PATTERN_LEN) {
log_message(LOG_LEVEL_ERROR, "invalid per-directory filter pattern");
return false;
}
}
int groups = 0;
for (int i = 0; i < count;) {
const char* owner = filter_dir_rule_owner(rules->items[i]);
groups++;
i++;
while (i < count && strcmp(filter_dir_rule_owner(rules->items[i]), owner) == 0)
i++;
}
if (!send_int(fd, groups))
return false;
for (int i = 0; i < count;) {
const char* owner = filter_dir_rule_owner(rules->items[i]);
int start = i;
i++;
while (i < count && strcmp(filter_dir_rule_owner(rules->items[i]), owner) == 0)
i++;
if (!send_wire_str(fd, owner) || !send_int(fd, i - start))
return false;
for (int j = start; j < i; j++) {
const FilterRule* rule = rules->items[j];
if (!send_int(fd, (int)rule->action) || !send_int(fd, (int)rule->sides) ||
!send_int(fd, rule->anchored ? 1 : 0) || !send_int(fd, rule->dir_only ? 1 : 0) ||
!send_int(fd, rule->negate ? 1 : 0) || !send_int(fd, rule->no_inherit ? 1 : 0) ||
!send_wire_str(fd, rule->pattern))
return false;
}
}
return true;
}
/* Read one wire flag (an int restricted to 0/1). */
static bool receive_flag(int fd, bool* value) {
int raw;
if (!receive_int(fd, &raw) || (raw != 0 && raw != 1))
return false;
*value = raw != 0;
return true;
}
/* Read the per-directory filter block emitted by delete_filter_dir_rules_send.
* Reconstructs a flat FilterRuleList whose rules carry their owner directory;
* `*out` is NULL when the sender transmitted no rules. Every bound is enforced
* (group/rule counts, owner/pattern bytes, pattern length, action/sides domain)
* so a malicious peer can neither overread nor allocate unboundedly. Returns
* false on a malformed frame (the caller signals STATUS_ERROR). */
bool delete_filter_dir_rules_receive(int fd, FilterRuleList** out) {
if (!out)
return false;
*out = NULL;
int groups;
if (!receive_int(fd, &groups) || groups < 0 || groups > MAX_FILTER_RULES)
return false;
if (groups == 0)
return true;
FilterRuleList* list = filter_rule_list_create();
if (!list)
return false;
int total_rules = 0;
size_t bytes = 0;
for (int g = 0; g < groups; g++) {
char* dir = receive_wire_str(fd);
if (!dir)
goto fail;
size_t dir_bytes = strlen(dir);
if (!(dir[0] == '\0' || (dir[0] != '/' && !has_path_traversal(dir))) ||
dir_bytes > MAX_FILTER_BYTES - bytes) {
free(dir);
goto fail;
}
bytes += dir_bytes;
int rule_count;
if (!receive_int(fd, &rule_count) || rule_count < 0 || rule_count > MAX_FILTER_RULES ||
rule_count > MAX_FILTER_RULES - total_rules) {
free(dir);
goto fail;
}
for (int r = 0; r < rule_count; r++) {
int action, sides;
bool anchored, dir_only, negate, no_inherit;
if (!receive_int(fd, &action) ||
(action != FILTER_ACTION_EXCLUDE && action != FILTER_ACTION_INCLUDE) ||
!receive_int(fd, &sides) || sides < (int)FILTER_SIDE_SENDER ||
sides > (int)(FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER) ||
!receive_flag(fd, &anchored) || !receive_flag(fd, &dir_only) ||
!receive_flag(fd, &negate) || !receive_flag(fd, &no_inherit)) {
free(dir);
goto fail;
}
char* pattern = receive_wire_str(fd);
size_t pattern_bytes = pattern ? strlen(pattern) : 0;
if (!pattern || pattern_bytes == 0 || pattern_bytes > MAX_PROTECT_PATTERN_LEN ||
pattern_bytes > MAX_FILTER_BYTES - bytes) {
free(pattern);
free(dir);
goto fail;
}
bytes += pattern_bytes;
FilterRule* rule = calloc(1, sizeof(FilterRule));
if (!rule) {
free(pattern);
free(dir);
goto fail;
}
rule->action = (FilterAction)action;
rule->sides = (unsigned)sides;
rule->anchored = anchored;
rule->dir_only = dir_only;
rule->negate = negate;
rule->no_inherit = no_inherit;
rule->owner = str_dup(dir);
rule->pattern = pattern;
if (!rule->owner || !filter_rule_list_add(list, rule)) {
filter_rule_free(rule);
free(dir);
goto fail;
}
total_rules++;
}
free(dir);
}
*out = list;
return true;
fail:
filter_rule_list_free(list);
return false;
}
static int send_plan_node(int fd, DeletePlanSender* sender, PlanNode* node) { static int send_plan_node(int fd, DeletePlanSender* sender, PlanNode* node) {
if (!send_status(fd, STATUS_DELETE_PLAN)) if (!send_status(fd, STATUS_DELETE_PLAN))
return -1; return -1;
@@ -328,7 +485,8 @@ static int send_plan_node(int fd, DeletePlanSender* sender, PlanNode* node) {
if (!sender->config_sent) { if (!sender->config_sent) {
if (send_str_section(fd, sender->protected_prefixes) != 0 || if (send_str_section(fd, sender->protected_prefixes) != 0 ||
send_str_section(fd, sender->size_skipped) != 0 || send_str_section(fd, sender->size_skipped) != 0 ||
send_str_section(fd, sender->missing_args) != 0) send_str_section(fd, sender->missing_args) != 0 ||
!delete_filter_dir_rules_send(fd, sender->per_dir_rules))
return -1; return -1;
sender->config_sent = true; sender->config_sent = true;
} }
@@ -355,7 +513,8 @@ static int send_config_only(int fd, DeletePlanSender* sender) {
return -1; return -1;
if (send_str_section(fd, sender->protected_prefixes) != 0 || if (send_str_section(fd, sender->protected_prefixes) != 0 ||
send_str_section(fd, sender->size_skipped) != 0 || send_str_section(fd, sender->size_skipped) != 0 ||
send_str_section(fd, sender->missing_args) != 0) send_str_section(fd, sender->missing_args) != 0 ||
!delete_filter_dir_rules_send(fd, sender->per_dir_rules))
return -1; return -1;
sender->config_sent = true; sender->config_sent = true;
if (!send_int(fd, 0)) /* apply = false */ if (!send_int(fd, 0)) /* apply = false */
@@ -472,6 +631,10 @@ struct DeletePlanSession {
ArrayList* protected_prefixes; ArrayList* protected_prefixes;
ArrayList* size_skipped; ArrayList* size_skipped;
ArrayList* missing; ArrayList* missing;
/* Received per-directory filter rules (protocol 2.30.0), or NULL. Evaluated
deepest-directory-first for every candidate extra so a destination-only
entry matching only a per-directory rule is protected. */
FilterRuleList* per_dir_rules;
ArrayList* deferred; ArrayList* deferred;
DeletePathObserver observer; DeletePathObserver observer;
void* observer_context; void* observer_context;
@@ -532,6 +695,7 @@ void delete_plan_session_destroy(DeletePlanSession* session) {
array_list_delete(session->protected_prefixes); array_list_delete(session->protected_prefixes);
array_list_delete(session->size_skipped); array_list_delete(session->size_skipped);
array_list_delete(session->missing); array_list_delete(session->missing);
filter_rule_list_free(session->per_dir_rules);
array_list_delete(session->deferred); array_list_delete(session->deferred);
free(session); free(session);
} }
@@ -604,16 +768,18 @@ static int open_plan_dir(const Config* config, const char* dir) {
typedef struct { typedef struct {
DeleteSkipSet set; DeleteSkipSet set;
/* Receiver-side delete-protection rules received on the config frame (NULL /* Receiver-side delete-protection rules. `base` is the config-frame
when the sender sent none). Evaluated per extra so a protect/risk rule is command-line set and `dir` the received per-directory set (both NULL when
the sender sent none). Evaluated per extra so a protect/risk rule is
honored under --delete-during/--delete-delay exactly like the whole-tree honored under --delete-during/--delete-delay exactly like the whole-tree
commit walker. */ commit walker. */
const FilterRuleList* protect_rules; DeleteProtectRules protect;
} PlanSkips; } PlanSkips;
static bool build_plan_skips(const Config* config, const DeletePlanSession* session, static bool build_plan_skips(const Config* config, const DeletePlanSession* session,
PlanSkips* out) { PlanSkips* out) {
out->protect_rules = config->protect_rules; out->protect.base_rules = config->protect_rules;
out->protect.dir_rules = session->per_dir_rules;
/* The per-directory plan walk keeps each basis path verbatim (it does not /* The per-directory plan walk keeps each basis path verbatim (it does not
convert an absolute under-root path to its root-relative form, unlike the convert an absolute under-root path to its root-relative form, unlike the
whole-tree commit walk). */ whole-tree commit walk). */
@@ -778,10 +944,8 @@ static bool process_children(int dirfd, const char* dir_rel, const ArrayList* ke
bool is_dir = entries[i].is_dir; bool is_dir = entries[i].is_dir;
bool in_keep_dirs = is_dir && list_contains_str(keep_dirs, entries[i].name); bool in_keep_dirs = is_dir && list_contains_str(keep_dirs, entries[i].name);
bool in_keep_files = !is_dir && list_contains_str(keep_files, entries[i].name); bool in_keep_files = !is_dir && list_contains_str(keep_files, entries[i].name);
bool rule_protected = bool rule_protected = delete_protect_verdict(&skips->protect, child_rel, entries[i].name,
skips->protect_rules && is_dir) == FILTER_ACTION_PROTECT;
filter_rules_apply_side(skips->protect_rules, child_rel, entries[i].name, is_dir,
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT;
if (in_keep_dirs || in_keep_files || rule_protected) { if (in_keep_dirs || in_keep_files || rule_protected) {
shielded[i] = true; shielded[i] = true;
local_survives = true; local_survives = true;
@@ -903,7 +1067,8 @@ int delete_plan_session_receive(DeletePlanSession* session, const Config* config
if (has_config) { if (has_config) {
if (session->config_seen || !read_section(fd, session->protected_prefixes, true, &bytes) || if (session->config_seen || !read_section(fd, session->protected_prefixes, true, &bytes) ||
!read_section(fd, session->size_skipped, true, &bytes) || !read_section(fd, session->size_skipped, true, &bytes) ||
!read_section(fd, session->missing, true, &bytes)) { !read_section(fd, session->missing, true, &bytes) ||
!delete_filter_dir_rules_receive(fd, &session->per_dir_rules)) {
send_status(fd, STATUS_ERROR); send_status(fd, STATUS_ERROR);
return -1; return -1;
} }
+15 -1
View File
@@ -25,6 +25,19 @@
* --delete-missing-args exact deletions, the shared --max-delete budget and, * --delete-missing-args exact deletions, the shared --max-delete budget and,
* for --delete-delay, the snapshotted extras. */ * for --delete-delay, the snapshotted extras. */
/* Per-directory filter-rule block (protocol 2.30.0). The sender compiles the
* source's per-directory merge rules as it scans and streams them so the
* receiver can re-derive the receiver-side protect/risk verdicts for
* destination-only entries. The wire format is a group count, then for each
* directory group its relative owner path followed by that directory's rule
* records (action, sides, anchored, dir-only, negate, no-inherit, pattern).
* All bounds (MAX_FILTER_RULES, MAX_FILTER_BYTES, MAX_PROTECT_PATTERN_LEN) are
* enforced on both sides; a malformed receive frame signals STATUS_ERROR and
* returns false. Receive yields a flat FilterRuleList whose rules carry their
* owner, or NULL when no rules were sent. */
bool delete_filter_dir_rules_send(int fd, const FilterRuleList* rules);
bool delete_filter_dir_rules_receive(int fd, FilterRuleList** out);
/* ---- Sender: plan builder ---- */ /* ---- Sender: plan builder ---- */
typedef struct DeletePlanSender DeletePlanSender; typedef struct DeletePlanSender DeletePlanSender;
@@ -53,7 +66,8 @@ bool delete_plan_sender_empty(const DeletePlanSender* sender);
* block is always transmitted by delete_plan_send_root(), on a config-only * block is always transmitted by delete_plan_send_root(), on a config-only
* carrier frame when the scope allows no directory plan. */ * carrier frame when the scope allows no directory plan. */
void delete_plan_sender_set_config(DeletePlanSender* sender, const ArrayList* protected_prefixes, void delete_plan_sender_set_config(DeletePlanSender* sender, const ArrayList* protected_prefixes,
const ArrayList* size_skipped, const ArrayList* missing_args); const ArrayList* size_skipped, const ArrayList* missing_args,
const FilterRuleList* per_dir_rules);
/* Send the root plan (even before any data, so root extras are handled like /* Send the root plan (even before any data, so root extras are handled like
* rsync's first generator directory), after transmitting the per-run config * rsync's first generator directory), after transmitting the per-run config
* block on its own carrier frame. Returns -1 on I/O error. */ * block on its own carrier frame. Returns -1 on I/O error. */
+298 -83
View File
@@ -11,6 +11,8 @@
/* Write a diagnostic message into the caller's optional buffer. */ /* Write a diagnostic message into the caller's optional buffer. */
#define filter_set_error utils_set_error #define filter_set_error utils_set_error
static bool set_rule_owner(FilterRule* rule, const char* owner);
/* ---- Ordered rule lists ---- */ /* ---- Ordered rule lists ---- */
void filter_rule_free(FilterRule* rule) { void filter_rule_free(FilterRule* rule) {
@@ -21,6 +23,28 @@ void filter_rule_free(FilterRule* rule) {
free(rule); free(rule);
} }
FilterRule* filter_rule_clone(const FilterRule* rule) {
if (!rule)
return NULL;
FilterRule* copy = calloc(1, sizeof(FilterRule));
if (!copy)
return NULL;
copy->action = rule->action;
copy->sides = rule->sides;
copy->anchored = rule->anchored;
copy->dir_only = rule->dir_only;
copy->negate = rule->negate;
copy->perishable = rule->perishable;
copy->no_inherit = rule->no_inherit;
copy->owner = str_dup(rule->owner ? rule->owner : "");
copy->pattern = str_dup(rule->pattern ? rule->pattern : "");
if (!copy->owner || !copy->pattern) {
filter_rule_free(copy);
return NULL;
}
return copy;
}
FilterRuleList* filter_rule_list_create(void) { FilterRuleList* filter_rule_list_create(void) {
return calloc(1, sizeof(FilterRuleList)); return calloc(1, sizeof(FilterRuleList));
} }
@@ -48,33 +72,76 @@ void filter_rule_list_free(FilterRuleList* list) {
for (int i = 0; i < list->count; i++) for (int i = 0; i < list->count; i++)
filter_rule_free(list->items[i]); filter_rule_free(list->items[i]);
for (int i = 0; i < list->dir_merge_count; i++) for (int i = 0; i < list->dir_merge_count; i++)
free(list->dir_merge_names[i]); free(list->dir_merges[i].name);
free(list->dir_merge_names); free(list->dir_merges);
free(list->items); free(list->items);
free(list); free(list);
} }
/* Append an implicit exclude rule for the merge file itself (rsync's 'e'
* modifier). The rule is owned by the transfer root and matches the basename
* anywhere, exactly like rsync's EXCLUDE_SELF (a dual-sided exclude: it hides
* the file and protects its destination mirror from --delete). */
static bool filter_list_add_exclude_self(FilterRuleList* list, const char* name) {
const char* base = strrchr(name, '/');
base = base ? base + 1 : name;
if (base[0] == '\0')
return true;
FilterRule* rule = calloc(1, sizeof(FilterRule));
if (!rule)
return false;
rule->action = FILTER_ACTION_EXCLUDE;
rule->sides = FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER;
rule->pattern = str_dup(base);
if (!rule->pattern || !set_rule_owner(rule, "")) {
filter_rule_free(rule);
return false;
}
if (!filter_rule_list_add(list, rule)) {
filter_rule_free(rule);
return false;
}
return true;
}
/* Register a per-directory merge-file basename (for "dir-merge NAME"/": NAME" /* Register a per-directory merge-file basename (for "dir-merge NAME"/": NAME"
* and -F's .rsync-filter). Duplicate names are ignored. */ * and -F's .rsync-filter). Duplicate names are ignored. */
bool filter_rule_list_add_dir_merge(FilterRuleList* list, const char* name) { bool filter_rule_list_add_dir_merge(FilterRuleList* list, const char* name) {
return filter_rule_list_add_dir_merge_ex(list, name, false, false, false, false, false);
}
bool filter_rule_list_add_dir_merge_ex(FilterRuleList* list, const char* name, bool no_prefixes,
bool include, bool word_split, bool no_inherit,
bool exclude_self) {
if (!list || !name || name[0] == '\0') if (!list || !name || name[0] == '\0')
return false; return false;
for (int i = 0; i < list->dir_merge_count; i++) { for (int i = 0; i < list->dir_merge_count; i++) {
if (strcmp(list->dir_merge_names[i], name) == 0) if (strcmp(list->dir_merges[i].name, name) == 0)
return true; return true;
} }
if (list->dir_merge_count == list->dir_merge_capacity) { if (list->dir_merge_count == list->dir_merge_capacity) {
int new_cap = list->dir_merge_capacity > 0 ? list->dir_merge_capacity * 2 : 4; int new_cap = list->dir_merge_capacity > 0 ? list->dir_merge_capacity * 2 : 4;
char** grown = realloc(list->dir_merge_names, (size_t)new_cap * sizeof(char*)); FilterDirMerge* grown = realloc(list->dir_merges, (size_t)new_cap * sizeof(*grown));
if (!grown) if (!grown)
return false; return false;
list->dir_merge_names = grown; list->dir_merges = grown;
list->dir_merge_capacity = new_cap; list->dir_merge_capacity = new_cap;
} }
char* dup = str_dup(name); char* dup = str_dup(name);
if (!dup) if (!dup)
return false; return false;
list->dir_merge_names[list->dir_merge_count++] = dup; if (exclude_self && !filter_list_add_exclude_self(list, name)) {
free(dup);
return false;
}
FilterDirMerge* entry = &list->dir_merges[list->dir_merge_count];
entry->name = dup;
entry->no_prefixes = no_prefixes;
entry->include = include;
entry->word_split = word_split;
entry->no_inherit = no_inherit;
entry->exclude_self = exclude_self;
list->dir_merge_count++;
return true; return true;
} }
@@ -177,10 +244,11 @@ static bool is_unsupported_modifier_char(char c) {
return c == 'e' || c == 'n' || c == 'w'; return c == 'e' || c == 'n' || c == 'w';
} }
/* Merge-file modifiers rsync accepts on merge/dir-merge rules: 'e', 'n', 'w' /* Merge-file modifiers rsync accepts on merge/dir-merge rules: 'e' (exclude
* and '-' (do not transfer the merge file). */ * self), 'n' (no inherit), 'w' (word split), '-' (bare excludes) and '+'
* (bare includes). */
static bool is_merge_modifier_char(char c) { static bool is_merge_modifier_char(char c) {
return c == 'e' || c == 'n' || c == 'w' || c == '-'; return c == 'e' || c == 'n' || c == 'w' || c == '-' || c == '+';
} }
/* Characters that count as part of a modifier run for `kind` when deciding /* Characters that count as part of a modifier run for `kind` when deciding
@@ -228,8 +296,10 @@ static char unsupported_modifier_in_token(const char* tok, RuleKind kind) {
* generic syntax error so callers can emit a precise diagnostic. */ * generic syntax error so callers can emit a precise diagnostic. */
static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides, static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides,
bool* sides_explicit, bool* negate, bool* anchored_mod, bool* sides_explicit, bool* negate, bool* anchored_mod,
bool* perishable, bool* xattr, bool* cvs_inject, bool* perishable, bool* xattr, bool* cvs_inject, bool* no_prefixes,
const char** pat_start, size_t* pat_len, char* bad_mod) { bool* include_defaults, bool* word_split, bool* no_inherit,
bool* exclude_self, const char** pat_start, size_t* pat_len,
char* bad_mod) {
const char* p = text; const char* p = text;
*sides = FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER; *sides = FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER;
*sides_explicit = false; *sides_explicit = false;
@@ -238,6 +308,11 @@ static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides,
*perishable = false; *perishable = false;
*xattr = false; *xattr = false;
*cvs_inject = false; *cvs_inject = false;
*no_prefixes = false;
*include_defaults = false;
*word_split = false;
*no_inherit = false;
*exclude_self = false;
*pat_start = NULL; *pat_start = NULL;
*pat_len = 0; *pat_len = 0;
*bad_mod = '\0'; *bad_mod = '\0';
@@ -312,6 +387,21 @@ static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides,
case 'C': case 'C':
*cvs_inject = true; *cvs_inject = true;
break; break;
case '-':
*no_prefixes = true;
break;
case '+':
*include_defaults = true;
break;
case 'e':
*exclude_self = true;
break;
case 'n':
*no_inherit = true;
break;
case 'w':
*word_split = true;
break;
default: default:
break; break;
} }
@@ -347,11 +437,13 @@ FilterRule* filter_rule_parse(const char* line, const FilterParseOptions* opts,
RuleKind kind = RULE_KIND_UNKNOWN; RuleKind kind = RULE_KIND_UNKNOWN;
unsigned sides; unsigned sides;
bool sides_explicit, negate, anchored_mod, perishable, xattr, cvs_inject; bool sides_explicit, negate, anchored_mod, perishable, xattr, cvs_inject;
bool no_prefixes, include_defaults, word_split, no_inherit, exclude_self;
const char* pat; const char* pat;
size_t pat_len; size_t pat_len;
char bad_mod; char bad_mod;
if (!parse_rule_syntax(p, &kind, &sides, &sides_explicit, &negate, &anchored_mod, &perishable, if (!parse_rule_syntax(p, &kind, &sides, &sides_explicit, &negate, &anchored_mod, &perishable,
&xattr, &cvs_inject, &pat, &pat_len, &bad_mod)) { &xattr, &cvs_inject, &no_prefixes, &include_defaults, &word_split,
&no_inherit, &exclude_self, &pat, &pat_len, &bad_mod)) {
if (bad_mod != '\0') if (bad_mod != '\0')
filter_set_error(err, err_size, "unsupported filter modifier '%c'", bad_mod); filter_set_error(err, err_size, "unsupported filter modifier '%c'", bad_mod);
else else
@@ -522,16 +614,130 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
const FilterParseOptions* opts, const char* base_dir, const FilterParseOptions* opts, const char* base_dir,
int depth, char* err, size_t err_size); int depth, char* err, size_t err_size);
/* Read a merge file and splice its rules into `list`. A relative path is /* Append one merge-file token/line to `list`, honoring the merge rule's
* resolved below `base_dir` when given, else used as-is (rsync resolves a * no-prefix/include mode. In no-prefix mode the token is a bare pattern whose
* command-line merge file relative to the current directory). */ * include/exclude default comes from the merge rule (rsync's "-"/"+" merge
* modifiers); otherwise the token is parsed as a full filter rule. */
static bool filter_merge_append_token(FilterRuleList* list, const char* token,
const FilterDirMerge* spec, const FilterParseOptions* opts,
const char* base_dir, int depth, char* err, size_t err_size) {
if (spec->no_prefixes || spec->include) {
size_t tlen = strlen(token);
char* text = malloc(tlen + 3);
if (!text) {
filter_set_error(err, err_size, "memory allocation failed");
return false;
}
text[0] = spec->include ? '+' : '-';
text[1] = ' ';
memcpy(text + 2, token, tlen + 1);
bool ok = filter_list_parse_append_depth(list, text, opts, base_dir, depth + 1, err, err_size);
free(text);
return ok;
}
return filter_list_parse_append_depth(list, token, opts, base_dir, depth + 1, err, err_size);
}
/* Read a merge file's tokens/lines into `list` for `spec`. A `w` merge rule
* word-splits on whitespace (turning comments off); otherwise lines are parsed
* and whole-line `#` comments skipped. When `owner_rel` is non-NULL the newly
* added rules are owned by that directory; a no-inherit spec marks them so they
* apply only there. Returns false on parse/allocation failure. */
static bool filter_merge_read(FilterRuleList* list, FILE* fp, const char* display_path,
const FilterDirMerge* spec, const FilterParseOptions* opts,
const char* base_dir, const char* owner_rel, int depth, char* err,
size_t err_size) {
int rules_before = list->count;
char* line = NULL;
size_t cap = 0;
bool ok = true;
while (ok) {
ssize_t n = utils_getdelim_bounded(fp, &line, &cap, '\n', UTILS_MAX_LINE_LEN);
if (n < 0) {
if (errno == EFBIG)
filter_set_error(err, err_size, "line in %s exceeds %d bytes", display_path,
(int)UTILS_MAX_LINE_LEN);
else
filter_set_error(err, err_size, "error reading %s: %s", display_path, strerror(errno));
ok = false;
break;
}
if (n == 0)
break;
if (spec->word_split) {
/* Whitespace-separated tokens; newlines are ordinary separators and
* comments are disabled. */
const char* s = line;
while (*s) {
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r')
s++;
if (*s == '\0')
break;
const char* start = s;
while (*s != '\0' && *s != ' ' && *s != '\t' && *s != '\n' && *s != '\r')
s++;
size_t tlen = (size_t)(s - start);
char* token = malloc(tlen + 1);
if (!token) {
filter_set_error(err, err_size, "memory allocation failed");
ok = false;
break;
}
memcpy(token, start, tlen);
token[tlen] = '\0';
if (!filter_merge_append_token(list, token, spec, opts, base_dir, depth, err, err_size))
ok = false;
free(token);
}
} else {
const char* lp = line;
while (*lp == ' ' || *lp == '\t')
lp++;
if (*lp == '\0' || *lp == '\n' || *lp == '\r' || *lp == '#')
continue;
if (!filter_merge_append_token(list, lp, spec, opts, base_dir, depth, err, err_size))
ok = false;
}
}
free(line);
if (!ok) {
/* Drop the rules this read appended (a "clear" inside the file may have
* freed earlier rules too; clamp like filter_file_rollback). */
int first = rules_before < list->count ? rules_before : list->count;
for (int i = first; i < list->count; i++)
filter_rule_free(list->items[i]);
list->count = first;
return false;
}
for (int i = rules_before; i < list->count; i++) {
FilterRule* rule = list->items[i];
if (spec->no_inherit)
rule->no_inherit = true;
if (owner_rel && !set_rule_owner(rule, owner_rel)) {
filter_set_error(err, err_size, "memory allocation failed");
for (int j = rules_before; j < list->count; j++)
filter_rule_free(list->items[j]);
list->count = rules_before;
return false;
}
}
return true;
}
/* Read a single-instance merge file and splice its rules into `list`. A
* relative path is resolved below `base_dir` when given, else used as-is (rsync
* resolves a command-line merge file relative to the current directory). */
static bool filter_list_merge_file(FilterRuleList* list, const char* name, static bool filter_list_merge_file(FilterRuleList* list, const char* name,
const FilterParseOptions* opts, const char* base_dir, int depth, const FilterDirMerge* spec, const FilterParseOptions* opts,
char* err, size_t err_size) { const char* base_dir, int depth, char* err, size_t err_size) {
if (name[0] == '\0') { if (name[0] == '\0') {
filter_set_error(err, err_size, "merge requires a filename"); filter_set_error(err, err_size, "merge requires a filename");
return false; return false;
} }
if (spec->exclude_self && !filter_list_add_exclude_self(list, name)) {
filter_set_error(err, err_size, "memory allocation failed");
return false;
}
char* path = char* path =
(base_dir && base_dir[0] && name[0] != '/') ? path_cat(base_dir, name) : str_dup(name); (base_dir && base_dir[0] && name[0] != '/') ? path_cat(base_dir, name) : str_dup(name);
if (!path) { if (!path) {
@@ -544,29 +750,7 @@ static bool filter_list_merge_file(FilterRuleList* list, const char* name,
free(path); free(path);
return false; return false;
} }
char* line = NULL; bool ok = filter_merge_read(list, fp, path, spec, opts, base_dir, NULL, depth, err, err_size);
size_t cap = 0;
bool ok = true;
while (true) {
ssize_t n = utils_getdelim_bounded(fp, &line, &cap, '\n', UTILS_MAX_LINE_LEN);
if (n < 0) {
filter_set_error(err, err_size, "error reading merge file '%s'", path);
ok = false;
break;
}
if (n == 0)
break;
const char* lp = line;
while (*lp == ' ' || *lp == '\t')
lp++;
if (*lp == '\0' || *lp == '\n' || *lp == '\r' || *lp == '#')
continue;
if (!filter_list_parse_append_depth(list, lp, opts, base_dir, depth + 1, err, err_size)) {
ok = false;
break;
}
}
free(line);
fclose(fp); fclose(fp);
free(path); free(path);
return ok; return ok;
@@ -590,11 +774,13 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
RuleKind kind = RULE_KIND_UNKNOWN; RuleKind kind = RULE_KIND_UNKNOWN;
unsigned sides; unsigned sides;
bool sides_explicit, negate, anchored_mod, perishable, xattr, cvs_inject; bool sides_explicit, negate, anchored_mod, perishable, xattr, cvs_inject;
bool no_prefixes, include_defaults, word_split, no_inherit, exclude_self;
const char* pat; const char* pat;
size_t pat_len; size_t pat_len;
char bad_mod; char bad_mod;
if (!parse_rule_syntax(p, &kind, &sides, &sides_explicit, &negate, &anchored_mod, &perishable, if (!parse_rule_syntax(p, &kind, &sides, &sides_explicit, &negate, &anchored_mod, &perishable,
&xattr, &cvs_inject, &pat, &pat_len, &bad_mod)) { &xattr, &cvs_inject, &no_prefixes, &include_defaults, &word_split,
&no_inherit, &exclude_self, &pat, &pat_len, &bad_mod)) {
if (bad_mod != '\0') if (bad_mod != '\0')
filter_set_error(err, err_size, "unsupported filter modifier '%c': %s", bad_mod, p); filter_set_error(err, err_size, "unsupported filter modifier '%c': %s", bad_mod, p);
else else
@@ -640,7 +826,15 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
} }
memcpy(name, pat, pat_len); memcpy(name, pat, pat_len);
name[pat_len] = '\0'; name[pat_len] = '\0';
bool ok = filter_list_merge_file(list, name, opts, base_dir, depth, err, err_size); /* A single-instance merge has no inheritance, so 'n' is meaningless; the
* other merge modifiers still shape how the file is read. */
FilterDirMerge spec = {.name = name,
.no_prefixes = no_prefixes,
.include = include_defaults,
.word_split = word_split,
.no_inherit = false,
.exclude_self = exclude_self};
bool ok = filter_list_merge_file(list, name, &spec, opts, base_dir, depth, err, err_size);
free(name); free(name);
return ok; return ok;
} }
@@ -656,7 +850,8 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
} }
memcpy(name, pat, pat_len); memcpy(name, pat, pat_len);
name[pat_len] = '\0'; name[pat_len] = '\0';
bool ok = filter_rule_list_add_dir_merge(list, name); bool ok = filter_rule_list_add_dir_merge_ex(list, name, no_prefixes, include_defaults,
word_split, no_inherit, exclude_self);
free(name); free(name);
if (!ok) { if (!ok) {
filter_set_error(err, err_size, "memory allocation failed"); filter_set_error(err, err_size, "memory allocation failed");
@@ -714,6 +909,10 @@ FilterRuleList* filter_base_build(const char* const* rule_texts, int rule_count,
/* ---- Per-directory merge files ---- */ /* ---- Per-directory merge files ---- */
/* Undo the rules and dir-merge registrations that one merge file appended,
* leaving the caller's earlier content intact. A "clear" rule inside the file
* frees every rule, including the caller's; clamp to the surviving count so
* those already-freed rules are never resurrected and freed a second time. */
/* Undo the rules and dir-merge registrations that one merge file appended, /* Undo the rules and dir-merge registrations that one merge file appended,
* leaving the caller's earlier content intact. A "clear" rule inside the file * leaving the caller's earlier content intact. A "clear" rule inside the file
* frees every rule, including the caller's; clamp to the surviving count so * frees every rule, including the caller's; clamp to the surviving count so
@@ -724,20 +923,20 @@ static void filter_file_rollback(FilterRuleList* list, int rules_before, int dir
filter_rule_free(list->items[i]); filter_rule_free(list->items[i]);
list->count = first; list->count = first;
for (int i = dir_merges_before; i < list->dir_merge_count; i++) for (int i = dir_merges_before; i < list->dir_merge_count; i++)
free(list->dir_merge_names[i]); free(list->dir_merges[i].name);
list->dir_merge_count = dir_merges_before; list->dir_merge_count = dir_merges_before;
} }
bool filter_file_append(FilterRuleList* list, const char* dir_path, const char* name, bool filter_dir_merge_append(FilterRuleList* list, const char* dir_path, const FilterDirMerge* spec,
const char* owner_rel, const FilterParseOptions* opts, bool* exists, const char* owner_rel, const FilterParseOptions* opts, bool* exists,
char* err, size_t err_size) { char* err, size_t err_size) {
if (err && err_size > 0) if (err && err_size > 0)
err[0] = '\0'; err[0] = '\0';
if (exists) if (exists)
*exists = false; *exists = false;
if (!list) if (!list || !spec || !spec->name)
return false; return false;
char* filter_path = path_cat(dir_path, name); char* filter_path = path_cat(dir_path, spec->name);
if (!filter_path) { if (!filter_path) {
filter_set_error(err, err_size, "memory allocation failed"); filter_set_error(err, err_size, "memory allocation failed");
return false; return false;
@@ -748,7 +947,7 @@ bool filter_file_append(FilterRuleList* list, const char* dir_path, const char*
if (errno == ENOENT || errno == ENOTDIR) if (errno == ENOENT || errno == ENOTDIR)
return true; return true;
char* escaped_dir = output_escape(dir_path, log_get_8_bit_output()); char* escaped_dir = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Could not read %s in %s: %s", name, log_message(LOG_LEVEL_WARNING, "Could not read %s in %s: %s", spec->name,
escaped_dir ? escaped_dir : "<allocation failed>", strerror(errno)); escaped_dir ? escaped_dir : "<allocation failed>", strerror(errno));
free(escaped_dir); free(escaped_dir);
return true; return true;
@@ -757,51 +956,25 @@ bool filter_file_append(FilterRuleList* list, const char* dir_path, const char*
*exists = true; *exists = true;
int rules_before = list->count; int rules_before = list->count;
int dir_merges_before = list->dir_merge_count; int dir_merges_before = list->dir_merge_count;
char* line = NULL;
size_t line_cap = 0;
bool ok = true;
while (true) {
ssize_t n = utils_getdelim_bounded(fp, &line, &line_cap, '\n', UTILS_MAX_LINE_LEN);
if (n < 0) {
if (errno == EFBIG) {
filter_set_error(err, err_size, "line in %s exceeds %d bytes", name,
(int)UTILS_MAX_LINE_LEN);
} else {
filter_set_error(err, err_size, "error reading %s: %s", name, strerror(errno));
}
ok = false;
break;
}
if (n == 0)
break;
const char* p = line;
while (*p == ' ' || *p == '\t')
p++;
if (*p == '\0' || *p == '\n' || *p == '\r' || *p == '#')
continue;
/* Merge files inside a per-directory file resolve relative to that /* Merge files inside a per-directory file resolve relative to that
directory. */ directory. */
if (!filter_list_parse_append_depth(list, p, opts, dir_path, 0, err, err_size)) { bool ok =
ok = false; filter_merge_read(list, fp, spec->name, spec, opts, dir_path, owner_rel, 0, err, err_size);
break;
}
}
free(line);
fclose(fp); fclose(fp);
if (!ok) { if (!ok) {
filter_file_rollback(list, rules_before, dir_merges_before); filter_file_rollback(list, rules_before, dir_merges_before);
return false; return false;
} }
for (int i = rules_before; i < list->count; i++) {
if (!set_rule_owner(list->items[i], owner_rel)) {
filter_set_error(err, err_size, "memory allocation failed");
filter_file_rollback(list, rules_before, dir_merges_before);
return false;
}
}
return true; return true;
} }
bool filter_file_append(FilterRuleList* list, const char* dir_path, const char* name,
const char* owner_rel, const FilterParseOptions* opts, bool* exists,
char* err, size_t err_size) {
FilterDirMerge spec = {.name = (char*)name};
return filter_dir_merge_append(list, dir_path, &spec, owner_rel, opts, exists, err, err_size);
}
FilterRuleList* filter_file_read_named(const char* dir_path, const char* name, FilterRuleList* filter_file_read_named(const char* dir_path, const char* name,
const char* owner_rel, const FilterParseOptions* opts, const char* owner_rel, const FilterParseOptions* opts,
bool* exists, char* err, size_t err_size) { bool* exists, char* err, size_t err_size) {
@@ -855,6 +1028,10 @@ static FilterAction rule_matches(const FilterRule* rule, const char* rel_path, c
} }
if (rel2[0] == '\0') if (rel2[0] == '\0')
return FILTER_ACTION_NONE; return FILTER_ACTION_NONE;
/* A no-inherit rule ('n' on its dir-merge) applies only to direct children of
* its owner directory, never to deeper entries. */
if (rule->no_inherit && strchr(rel2, '/') != NULL)
return FILTER_ACTION_NONE;
bool matched; bool matched;
if (rule->dir_only && !is_dir) if (rule->dir_only && !is_dir)
matched = false; matched = false;
@@ -884,3 +1061,41 @@ FilterAction filter_rules_apply_side(const FilterRuleList* list, const char* rel
} }
return FILTER_ACTION_NONE; return FILTER_ACTION_NONE;
} }
FilterAction filter_dir_rules_apply_side(const FilterRuleList* dir_rules, const char* rel_path,
const char* leaf, bool is_dir) {
if (!dir_rules || !rel_path)
return FILTER_ACTION_NONE;
size_t len = strlen(rel_path);
if (len == 0)
return FILTER_ACTION_NONE;
/* The containing directory of rel_path is the prefix before its final '/'. */
size_t owner_len = 0;
for (size_t i = 0; i < len; i++) {
if (rel_path[i] == '/')
owner_len = i;
}
for (;;) {
for (int i = 0; i < dir_rules->count; i++) {
const FilterRule* rule = dir_rules->items[i];
size_t rule_owner_len = rule && rule->owner ? strlen(rule->owner) : 0;
if (rule_owner_len != owner_len)
continue;
if (owner_len != 0 && memcmp(rule->owner, rel_path, owner_len) != 0)
continue;
FilterAction action = rule_matches(rule, rel_path, leaf, is_dir, FILTER_SIDE_RECEIVER);
if (action != FILTER_ACTION_NONE)
return action;
}
if (owner_len == 0)
break;
/* Move to the parent directory: the last '/' before owner_len. */
size_t parent = 0;
for (size_t j = 0; j < owner_len; j++) {
if (rel_path[j] == '/')
parent = j;
}
owner_len = parent;
}
return FILTER_ACTION_NONE;
}
+56 -11
View File
@@ -25,11 +25,12 @@
* Modifiers: '/' absolute anchor, '!' negate match, 'C' inject CVS defaults, * Modifiers: '/' absolute anchor, '!' negate match, 'C' inject CVS defaults,
* 's' sender side, 'r' receiver side, 'p' perishable. The rsync 'x' * 's' sender side, 'r' receiver side, 'p' perishable. The rsync 'x'
* (xattr-name) modifier is not implemented and is rejected explicitly * (xattr-name) modifier is not implemented and is rejected explicitly
* everywhere. The merge-file modifiers 'e' (exclude the merge file itself), * everywhere. The merge-only modifiers are accepted only on merge/dir-merge
* 'n' (do not inherit the merge file), 'w' (word-split the merge file) and '-' * rules (rejected on every other rule, matching rsync): 'e' excludes the merge
* (do not transfer the merge file) are accepted and consumed only on merge/ * file itself, 'n' makes the merged rules non-inheriting (they apply only to
* dir-merge rules (rejected on every other rule, matching rsync); their * the directory that holds the merge file), 'w' word-splits the merge file on
* semantics are not implemented and they are otherwise ignored. * whitespace instead of lines, and '-' reads the merge file as a list of bare
* exclude patterns with no rule prefixes.
* A trailing '/' makes a pattern match directories only. A leading '/' anchors * A trailing '/' makes a pattern match directories only. A leading '/' anchors
* the pattern to its owner directory. * the pattern to its owner directory.
*/ */
@@ -55,18 +56,32 @@ typedef struct {
bool dir_only; /* pattern had a trailing '/': matches directories only */ bool dir_only; /* pattern had a trailing '/': matches directories only */
bool negate; /* '!' modifier: match succeeds when the pattern does not */ bool negate; /* '!' modifier: match succeeds when the pattern does not */
bool perishable; /* 'p' modifier (ignored in deleted directories) */ bool perishable; /* 'p' modifier (ignored in deleted directories) */
bool no_inherit; /* 'n' on the owning dir-merge: applies only in `owner` */
char* owner; /* owning directory rel path ("" == transfer root) */ char* owner; /* owning directory rel path ("" == transfer root) */
char* pattern; /* cleaned glob pattern (no leading '/', no trailing '/') */ char* pattern; /* cleaned glob pattern (no leading '/', no trailing '/') */
} FilterRule; } FilterRule;
/* One per-directory merge-file registration ("dir-merge NAME"/": NAME", "merge
* NAME"/". NAME" and -F's .rsync-filter) together with the merge-only modifiers
* parsed from the rule. The scanner reads `name` in every directory it
* traverses and applies `no_prefixes`/`include`/`word_split`/`no_inherit`/
* `exclude_self` while merging the file's rules. */
typedef struct {
char* name;
bool no_prefixes; /* '-' : file holds only bare exclude patterns */
bool include; /* '+' : file holds only bare include patterns */
bool word_split; /* 'w' : split the file on whitespace, not lines */
bool no_inherit; /* 'n' : the merged rules do not inherit below their dir */
bool exclude_self; /* 'e' : exclude the merge file itself from the transfer */
} FilterDirMerge;
typedef struct FilterRuleList { typedef struct FilterRuleList {
FilterRule** items; /* owned array of rule pointers */ FilterRule** items; /* owned array of rule pointers */
int count; int count;
int capacity; int capacity;
/* Per-directory merge-file basenames registered by "dir-merge NAME"/": NAME" /* Per-directory merge-file registrations. Owned; the scanner reads each
* or by -F (.rsync-filter). Owned strings; the scanner reads each name in * name in every directory it traverses. */
* every directory it traverses. */ FilterDirMerge* dir_merges;
char** dir_merge_names;
int dir_merge_count; int dir_merge_count;
int dir_merge_capacity; int dir_merge_capacity;
} FilterRuleList; } FilterRuleList;
@@ -83,13 +98,23 @@ typedef struct {
FilterRule* filter_rule_parse(const char* line, const FilterParseOptions* opts, char* err, FilterRule* filter_rule_parse(const char* line, const FilterParseOptions* opts, char* err,
size_t err_size); size_t err_size);
void filter_rule_free(FilterRule* rule); void filter_rule_free(FilterRule* rule);
/* Deep-copy a rule (owned pattern/owner). Returns NULL on allocation failure. */
FilterRule* filter_rule_clone(const FilterRule* rule);
FilterRuleList* filter_rule_list_create(void); FilterRuleList* filter_rule_list_create(void);
/* Append a fully-parsed rule (takes ownership). Returns false on OOM. */ /* Append a fully-parsed rule (takes ownership). Returns false on OOM. */
bool filter_rule_list_add(FilterRuleList* list, FilterRule* rule); bool filter_rule_list_add(FilterRuleList* list, FilterRule* rule);
/* Register a per-directory merge-file basename (idempotent). Returns false on /* Register a per-directory merge-file basename (idempotent, no modifiers).
* OOM. Used by the scanner to read custom "dir-merge" files. */ * Returns false on OOM. Used by the scanner to read custom "dir-merge" files. */
bool filter_rule_list_add_dir_merge(FilterRuleList* list, const char* name); bool filter_rule_list_add_dir_merge(FilterRuleList* list, const char* name);
/* Register a per-directory merge file with its merge-only modifiers. On a
* duplicate name the existing registration is kept (rsync's first wins) and true
* is returned. When `exclude_self` is set an implicit exclude rule for the
* merge file's basename is appended to the list at this position, matching
* rsync's `e` modifier. Returns false on OOM. */
bool filter_rule_list_add_dir_merge_ex(FilterRuleList* list, const char* name, bool no_prefixes,
bool include, bool word_split, bool no_inherit,
bool exclude_self);
/* Parse `line` and append it. Handles "clear"/"!" (resets the list), "merge /* Parse `line` and append it. Handles "clear"/"!" (resets the list), "merge
* FILE"/". FILE" (splices the file's rules) and "dir-merge NAME"/": NAME" * FILE"/". FILE" (splices the file's rules) and "dir-merge NAME"/": NAME"
* (registers a per-directory filename). Returns false and fills `err` on bad * (registers a per-directory filename). Returns false and fills `err` on bad
@@ -122,6 +147,15 @@ bool filter_file_append(FilterRuleList* list, const char* dir_path, const char*
const char* owner_rel, const FilterParseOptions* opts, bool* exists, const char* owner_rel, const FilterParseOptions* opts, bool* exists,
char* err, size_t err_size); char* err, size_t err_size);
/* Append a per-directory merge file honoring its merge-only modifiers: `-`
* reads every (word-split, when `w`) token as a bare exclude, `+` as a bare
* include, and `n` marks each read rule non-inheriting. A plain name behaves
* like filter_file_append. A missing file yields *exists=false with no error;
* returns false only on a parse/allocation failure (message in `err`). */
bool filter_dir_merge_append(FilterRuleList* list, const char* dir_path, const FilterDirMerge* spec,
const char* owner_rel, const FilterParseOptions* opts, bool* exists,
char* err, size_t err_size);
/* filter_file_read_named with the default ".rsync-filter" name. */ /* filter_file_read_named with the default ".rsync-filter" name. */
FilterRuleList* filter_file_read(const char* dir_path, const char* owner_rel, bool* exists, FilterRuleList* filter_file_read(const char* dir_path, const char* owner_rel, bool* exists,
char* err, size_t err_size); char* err, size_t err_size);
@@ -135,4 +169,15 @@ FilterRuleList* filter_file_read(const char* dir_path, const char* owner_rel, bo
FilterAction filter_rules_apply_side(const FilterRuleList* list, const char* rel_path, FilterAction filter_rules_apply_side(const FilterRuleList* list, const char* rel_path,
const char* leaf, bool is_dir, unsigned side); const char* leaf, bool is_dir, unsigned side);
/* Evaluate a received per-directory rule set for the receiver side, using
* rsync's per-directory-before-ancestors order: the entry's containing
* directory's rules are tried first, then each ancestor's, then the receive
* root's. `dir_rules` is a flat list whose rules carry `owner`; a rule applies
* only when `owner` is exactly the directory being examined (a no-inherit rule
* therefore applies only to that directory's direct children). Returns the
* first matching rule's receiver verdict (PROTECT/RISK) or FILTER_ACTION_NONE.
* The caller evaluates the command-line base rules after this chain. */
FilterAction filter_dir_rules_apply_side(const FilterRuleList* dir_rules, const char* rel_path,
const char* leaf, bool is_dir);
#endif #endif
+3
View File
@@ -34,6 +34,7 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
context->synced_dirs = NULL; context->synced_dirs = NULL;
context->plan_dirs = NULL; context->plan_dirs = NULL;
context->missing_args = NULL; context->missing_args = NULL;
context->per_dir_rules = NULL;
context->scan_had_io_error = false; context->scan_had_io_error = false;
context->remove_source_files = NULL; context->remove_source_files = NULL;
context->early_delete = false; context->early_delete = false;
@@ -210,6 +211,8 @@ void pipeline_context_sender_destroy(PipelineContextSender* context) {
array_list_delete(context->plan_dirs); array_list_delete(context->plan_dirs);
if (context->missing_args) if (context->missing_args)
array_list_delete(context->missing_args); array_list_delete(context->missing_args);
if (context->per_dir_rules)
filter_rule_list_free(context->per_dir_rules);
if (context->remove_source_files) if (context->remove_source_files)
array_list_delete(context->remove_source_files); array_list_delete(context->remove_source_files);
if (context->dir_entries) if (context->dir_entries)
+6
View File
@@ -67,6 +67,12 @@ typedef struct {
them in the manifest frame's third section and the receiver deletes each as them in the manifest frame's third section and the receiver deletes each as
an explicit request. */ an explicit request. */
ArrayList* missing_args; ArrayList* missing_args;
/* Per-directory filter rules the source scan compiled (protocol 2.30.0),
sent with the delete manifest/plan config so the receiver can re-derive the
per-directory protect/risk set. NULL when --delete is off. Populated by
the scanner (parallel workers append under mutex_scanner) or the early
pre-scan. */
FilterRuleList* per_dir_rules;
/* A source I/O error (unreadable directory) was recorded during the scan. /* A source I/O error (unreadable directory) was recorded during the scan.
Set by the pre-scan (before the threads start) or by the scanner thread Set by the pre-scan (before the threads start) or by the scanner thread
under mutex_scanner; the caller turns it into a non-zero exit when under mutex_scanner; the caller turns it into a non-zero exit when
@@ -128,6 +128,39 @@ def seed_filter_protect(_src, rroot, froot):
_mk(os.path.join(root, "sub", "other2.txt"), b"nested dest-only other\n", _OLD_MTIME) _mk(os.path.join(root, "sub", "other2.txt"), b"nested dest-only other\n", _OLD_MTIME)
def seed_perdir_protect(_src, rroot, froot):
"""Per-directory `.rsync-filter` carrying `P` rules on the source and both
destinations, plus destination-only extras. The `.log` extras must survive
--delete under every timing while the other extras go; the source's
`.rsync-filter` (which FastSync carries to the receiver) and the seeded
destination one (which rsync's receiver reads) are byte-identical."""
for root in (_src, rroot, froot):
_mk(os.path.join(root, ".rsync-filter"), b"P extra.log\nP nested.log\n")
for root in (rroot, froot):
_mk(os.path.join(root, "extra.log"), b"dest-only protected\n", _OLD_MTIME)
_mk(os.path.join(root, "other.txt"), b"dest-only deleted\n", _OLD_MTIME)
_mk(os.path.join(root, "sub", "nested.log"), b"nested protected\n", _OLD_MTIME)
_mk(os.path.join(root, "sub", "other2.txt"), b"nested deleted\n", _OLD_MTIME)
def seed_perdir_exclude(_src, rroot, froot):
"""Per-directory unqualified exclude (`-`): dual-sided, so it protects the
matching destination-only extra (and is opted back in by
--delete-excluded)."""
for root in (_src, rroot, froot):
_mk(os.path.join(root, ".rsync-filter"), b"- extra.log\n")
for root in (rroot, froot):
_mk(os.path.join(root, "extra.log"), b"dest-only excluded\n", _OLD_MTIME)
_mk(os.path.join(root, "other.txt"), b"dest-only deleted\n", _OLD_MTIME)
def _seed_rules(content):
def seed(_src, _rroot, _froot):
_mk(os.path.join(_src, ".rules"), content)
return seed
def seed_max_delete(_src, rroot, froot): def seed_max_delete(_src, rroot, froot):
for root in (rroot, froot): for root in (rroot, froot):
_mk(os.path.join(root, "extra1.txt"), b"e1\n", _OLD_MTIME) _mk(os.path.join(root, "extra1.txt"), b"e1\n", _OLD_MTIME)
@@ -272,6 +305,50 @@ _CASES = [
["-a", "--delete-after", "--filter=P *.log"], ["-a", "--delete-after", "--filter=P *.log"],
seed=seed_filter_protect, server_args=DELETE, ci=True, seed=seed_filter_protect, server_args=DELETE, ci=True,
ref="--filter P/--protect under the whole-tree --delete-after commit"), ref="--filter P/--protect under the whole-tree --delete-after commit"),
# Per-directory merge rules (#315): the receiver must re-derive the
# protect/risk verdict from the carried per-directory rules, so a
# destination-only entry matching ONLY a per-directory rule is shielded.
H.Case("filter_perdir_protect", "filters",
["-a", "-F", "--delete"],
seed=seed_perdir_protect, server_args=DELETE, ci=True,
ref="-F per-directory P rule under the default --delete timing"),
H.Case("filter_perdir_protect_during", "filters",
["-a", "-F", "--delete-during"],
seed=seed_perdir_protect, server_args=DELETE, ci=True,
ref="-F per-directory P rule under --delete-during"),
H.Case("filter_perdir_protect_delay", "filters",
["-a", "-F", "--delete-delay"],
seed=seed_perdir_protect, server_args=DELETE, ci=True,
ref="-F per-directory P rule under --delete-delay"),
H.Case("filter_perdir_protect_before", "filters",
["-a", "-F", "--delete-before"],
seed=seed_perdir_protect, server_args=DELETE, ci=True,
ref="-F per-directory P rule under the whole-tree --delete-before commit"),
H.Case("filter_perdir_protect_after", "filters",
["-a", "-F", "--delete-after"],
seed=seed_perdir_protect, server_args=DELETE, ci=True,
ref="-F per-directory P rule under the whole-tree --delete-after commit"),
H.Case("filter_perdir_exclude_protect", "filters",
["-a", "-F", "--delete"],
seed=seed_perdir_exclude, server_args=DELETE, ci=True,
ref="-F per-directory exclude protects its destination mirror"),
H.Case("filter_perdir_exclude_deleted", "filters",
["-a", "-F", "--delete", "--delete-excluded"],
seed=seed_perdir_exclude, server_args=DELETE, ci=True,
ref="-F per-directory exclude under --delete-excluded is at risk"),
# Merge-file modifiers (#315): e/n/w/- semantics match rsync 3.4.1.
H.Case("dir_merge_e", "filters", ["-a", "--filter=:e .rules"],
seed=_seed_rules(b"- *.log\n"), ci=True,
ref="dir-merge,e excludes the merge file itself"),
H.Case("dir_merge_n", "filters", ["-a", "--filter=:n .rules"],
seed=_seed_rules(b"- *.log\n"), ci=True,
ref="dir-merge,n does not inherit into subdirectories"),
H.Case("dir_merge_dash", "filters", ["-a", "--filter=:- .rules"],
seed=_seed_rules(b"*.log\n*.bin\n"), ci=True,
ref="dir-merge,- reads the file as bare exclude patterns"),
H.Case("dir_merge_w", "filters", ["-a", "--filter=:-w .rules"],
seed=_seed_rules(b"*.log *.bin\n"), ci=True,
ref="dir-merge,w word-splits bare patterns on whitespace"),
# --- relative / dirs -------------------------------------------------- # --- relative / dirs --------------------------------------------------
H.Case("relative_general", "basic", ["-a", "-R"], layout=H.MIRROR_ABS, H.Case("relative_general", "basic", ["-a", "-R"], layout=H.MIRROR_ABS,
+75
View File
@@ -536,3 +536,78 @@ class TestFilterProtect:
assert "extra.log" not in result.stdout, result.stdout assert "extra.log" not in result.stdout, result.stdout
assert os.path.exists(os.path.join(received, "extra.log")) assert os.path.exists(os.path.join(received, "extra.log"))
assert os.path.exists(os.path.join(received, "other.txt")) assert os.path.exists(os.path.join(received, "other.txt"))
@pytest.mark.ci
def test_perdir_protect_dest_only_matches_rsync(self):
"""#315: a `P` rule inside a per-directory `.rsync-filter` is carried to
the receiver, so a destination-only extra matching ONLY that rule is
shielded under --delete. Both roots carry the same filter file (rsync's
receiver reads the destination one; FastSync carries the source's)."""
source = os.path.join(TEST_DATA_DIR, "fpdp_src")
dest = os.path.join(TEST_DATA_DIR, "fpdp_dst")
rdst = os.path.join(TEST_DATA_DIR, "fpdp_rdst")
clean_dir(source)
_write(os.path.join(source, "keep.txt"), b"keep\n")
_write(os.path.join(source, ".rsync-filter"), b"P extra.log\n")
clean_dir(rdst)
_write(os.path.join(rdst, ".rsync-filter"), b"P extra.log\n")
_write(os.path.join(rdst, "extra.log"), b"extra\n")
_write(os.path.join(rdst, "other.txt"), b"other\n")
rsync_result = _rsync(["-aF", "--delete", source + "/", rdst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
assert os.path.exists(os.path.join(rdst, "extra.log")), "rsync did not protect extra.log"
assert not os.path.exists(os.path.join(rdst, "other.txt"))
clean_dir(dest)
received = get_dest_received_dir(dest, source)
os.makedirs(received, exist_ok=True)
_write(os.path.join(received, ".rsync-filter"), b"P extra.log\n")
_write(os.path.join(received, "extra.log"), b"extra\n")
_write(os.path.join(received, "other.txt"), b"other\n")
with ServerManager() as server:
server.start(extra_args=["--allow-delete"])
result, _ = run_client(source, dest, flags=["-aF", "--delete"], port=server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
assert os.path.exists(os.path.join(received, "extra.log")), (
"FastSync must protect a destination-only per-directory P match like rsync")
assert not os.path.exists(os.path.join(received, "other.txt"))
@requires_rsync
@pytest.mark.ci
def test_perdir_protect_dry_run_enumeration(self, shared_server):
"""#315: the -n/--dry-run would-delete enumeration also honors the
carried per-directory rules, matching rsync's `*deleting` set: a
destination-only entry matching only a `.rsync-filter` P rule is not
reported (nor removed)."""
source = os.path.join(TEST_DATA_DIR, "fpdp_nd_src")
dest = os.path.join(TEST_DATA_DIR, "fpdp_nd_dst")
rdst = os.path.join(TEST_DATA_DIR, "fpdp_nd_rdst")
clean_dir(source)
_write(os.path.join(source, "keep.txt"), b"keep\n")
_write(os.path.join(source, ".rsync-filter"), b"P extra.log\n")
received = get_dest_received_dir(dest, source)
clean_dir(rdst)
clean_dir(received)
for root in (rdst, received):
_write(os.path.join(root, "keep.txt"), b"keep\n")
_write(os.path.join(root, ".rsync-filter"), b"P extra.log\n")
_write(os.path.join(root, "extra.log"), b"extra\n")
_write(os.path.join(root, "other.txt"), b"other\n")
rsync_result = _rsync(["-an", "-i", "-F", "--delete", source + "/", rdst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
rsync_del = sorted(l for l in rsync_result.stdout.splitlines()
if l.startswith("*deleting"))
assert rsync_del == ["*deleting other.txt"], f"unexpected rsync set: {rsync_del}"
with ServerManager() as server:
server.start(extra_args=["--allow-delete"])
result, _ = run_client(source, dest, flags=["-aF", "-n", "-i", "--delete"],
port=server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
fs_del = sorted(l for l in (result.stdout or "").splitlines()
if l.startswith("*deleting"))
assert fs_del == rsync_del, f"rsync={rsync_del}\nfastsync={fs_del}"
assert os.path.exists(os.path.join(received, "extra.log"))
assert os.path.exists(os.path.join(received, "other.txt"))
+102
View File
@@ -225,6 +225,7 @@ static void send_config_only_frame(int fd, const char* missing_path) {
EXPECT_TRUE(send_int(fd, 0)); /* size-skipped */ EXPECT_TRUE(send_int(fd, 0)); /* size-skipped */
EXPECT_TRUE(send_int(fd, 1)); /* missing args */ EXPECT_TRUE(send_int(fd, 1)); /* missing args */
EXPECT_TRUE(send_wire_str(fd, missing_path)); EXPECT_TRUE(send_wire_str(fd, missing_path));
EXPECT_TRUE(send_int(fd, 0)); /* per-directory filter-rule block is empty */
EXPECT_TRUE(send_int(fd, 0)); /* apply = false */ EXPECT_TRUE(send_int(fd, 0)); /* apply = false */
EXPECT_TRUE(send_wire_str(fd, ".")); EXPECT_TRUE(send_wire_str(fd, "."));
EXPECT_TRUE(send_int(fd, 0)); EXPECT_TRUE(send_int(fd, 0));
@@ -265,10 +266,111 @@ static void test_config_only_frame_applies_missing_args(void) {
config_delete(config); config_delete(config);
} }
/* The per-directory filter-rule block (protocol 2.30.0) must be bounded on
* receive: every count, the action/sides domain, the owner-directory syntax and
* the pattern length are validated so a hostile peer can neither overread nor
* allocate unboundedly. It also round-trips a valid group faithfully. */
static void test_filter_dir_rules_receive_bounds(void) {
int p[2];
FilterRuleList* out = NULL;
/* Group count beyond the cap is rejected. */
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], MAX_FILTER_RULES + 1));
EXPECT_FALSE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NULL(out);
close(p[0]);
close(p[1]);
/* A negative group count is rejected. */
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], -1));
EXPECT_FALSE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NULL(out);
close(p[0]);
close(p[1]);
/* An empty block is valid and yields NULL. */
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NULL(out);
close(p[0]);
close(p[1]);
/* An unknown action is a protocol error. */
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_wire_str(p[1], ""));
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_int(p[1], 999));
EXPECT_FALSE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NULL(out);
close(p[0]);
close(p[1]);
/* An absolute owner directory is rejected (confinement). */
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_wire_str(p[1], "/etc"));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_FALSE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NULL(out);
close(p[0]);
close(p[1]);
/* An over-long pattern is rejected before allocation. */
{
char* big = malloc(MAX_PROTECT_PATTERN_LEN + 2);
EXPECT_NOT_NULL(big);
memset(big, 'a', MAX_PROTECT_PATTERN_LEN + 1);
big[MAX_PROTECT_PATTERN_LEN + 1] = '\0';
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_wire_str(p[1], ""));
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_int(p[1], (int)FILTER_ACTION_EXCLUDE));
EXPECT_TRUE(send_int(p[1], (int)(FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER)));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_wire_str(p[1], big));
EXPECT_FALSE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NULL(out);
close(p[0]);
close(p[1]);
free(big);
}
/* A valid group round-trips its owner, no-inherit flag and pattern. */
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_wire_str(p[1], "sub"));
EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_int(p[1], (int)FILTER_ACTION_EXCLUDE));
EXPECT_TRUE(send_int(p[1], (int)(FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER)));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_int(p[1], 0));
EXPECT_TRUE(send_int(p[1], 1)); /* no_inherit */
EXPECT_TRUE(send_wire_str(p[1], "*.log"));
EXPECT_TRUE(delete_filter_dir_rules_receive(p[0], &out));
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(out->count, 1);
EXPECT_EQ_STR(out->items[0]->owner, "sub");
EXPECT_EQ_STR(out->items[0]->pattern, "*.log");
EXPECT_TRUE(out->items[0]->no_inherit);
filter_rule_list_free(out);
close(p[0]);
close(p[1]);
}
void test_delete_plan(void) { void test_delete_plan(void) {
test_delete_delay_refilled_dir_removed_recursively(); test_delete_delay_refilled_dir_removed_recursively();
test_delete_delay_removed_file_counted(); test_delete_delay_removed_file_counted();
test_delete_delay_max_delete_bounds_actual(); test_delete_delay_max_delete_bounds_actual();
test_delete_delay_actual_removal_charges_budget(); test_delete_delay_actual_removal_charges_budget();
test_config_only_frame_applies_missing_args(); test_config_only_frame_applies_missing_args();
test_filter_dir_rules_receive_bounds();
} }
+172 -29
View File
@@ -1,6 +1,7 @@
#include "test_filter.h" #include "test_filter.h"
#include "filter.h" #include "filter.h"
#include "test_utils.h" #include "test_utils.h"
#include "utils.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -64,44 +65,47 @@ static void test_filter_list_rejects_unsupported_modifiers() {
} }
/* rsync accepts the merge-file modifiers e/n/w/- on merge and dir-merge rules. /* rsync accepts the merge-file modifiers e/n/w/- on merge and dir-merge rules.
* They must be consumed so they never leak into the merge filename. */ * They must be consumed so they never leak into the merge filename, and their
* semantics (exclude-self, no-inherit, word-split, no-prefixes) must be
* applied while the file is read. */
static void test_filter_list_accepts_merge_modifiers() { static void test_filter_list_accepts_merge_modifiers() {
char tmpl[] = "/tmp/fastsync_filter_mmod_XXXXXX"; char tmpl[] = "/tmp/fastsync_filter_mmod_XXXXXX";
EXPECT_TRUE(mkdtemp(tmpl) != NULL); EXPECT_TRUE(mkdtemp(tmpl) != NULL);
char path[512]; char prefixed[512];
snprintf(path, sizeof(path), "%s/rules", tmpl); char bare[512];
FILE* fp = fopen(path, "w"); char words[512];
snprintf(prefixed, sizeof(prefixed), "%s/prefixed", tmpl);
snprintf(bare, sizeof(bare), "%s/bare", tmpl);
snprintf(words, sizeof(words), "%s/words", tmpl);
FILE* fp = fopen(prefixed, "w");
EXPECT_NOT_NULL(fp); EXPECT_NOT_NULL(fp);
fputs("- *.tmp\n", fp); fputs("- *.tmp\n", fp);
fclose(fp); fclose(fp);
fp = fopen(bare, "w");
EXPECT_NOT_NULL(fp);
fputs("*.log\n*.tmp\n", fp);
fclose(fp);
fp = fopen(words, "w");
EXPECT_NOT_NULL(fp);
fputs("*.log *.tmp\n", fp);
fclose(fp);
/* merge with e/n/w/- consumes the modifiers and reads the right file. */ /* dir-merge with e/n/w/- registers the basename without the modifiers and
static const char* const fmts[] = { * records the modifier flags; 'e' appends an exclude-self rule. */
"merge,e %s", "merge,n %s", "merge,w %s", "merge,- %s", ".e %s", ".- %s",
};
for (size_t i = 0; i < sizeof(fmts) / sizeof(fmts[0]); i++) {
FilterRuleList* list = filter_rule_list_create();
EXPECT_NOT_NULL(list);
char rule[600];
char err[256] = "";
snprintf(rule, sizeof(rule), fmts[i], path);
bool ok = filter_rule_list_parse_append(list, rule, NULL, NULL, err, sizeof(err));
if (!ok)
printf(" merge rule '%s' errored: %s\n", rule, err);
EXPECT_TRUE(ok);
EXPECT_EQ_INT(list->count, 1);
EXPECT_EQ_STR(list->items[0]->pattern, "*.tmp");
filter_rule_list_free(list);
}
/* dir-merge with e/n/w/- registers the basename without the modifiers. */
static const struct { static const struct {
const char* rule; const char* rule;
const char* want; const char* want;
bool no_prefixes;
bool word_split;
bool no_inherit;
bool exclude_self;
} drules[] = { } drules[] = {
{"dir-merge,e .rules", ".rules"}, {"dir-merge,n .rules", ".rules"}, {"dir-merge,e .rules", ".rules", false, false, false, true},
{"dir-merge,w .rules", ".rules"}, {"dir-merge,- .rules", ".rules"}, {"dir-merge,n .rules", ".rules", false, false, true, false},
{":e .rules", ".rules"}, {":- .rules", ".rules"}, {"dir-merge,w .rules", ".rules", false, true, false, false},
{"dir-merge,- .rules", ".rules", true, false, false, false},
{":e .rules", ".rules", false, false, false, true},
{":- .rules", ".rules", true, false, false, false},
}; };
for (size_t i = 0; i < sizeof(drules) / sizeof(drules[0]); i++) { for (size_t i = 0; i < sizeof(drules) / sizeof(drules[0]); i++) {
FilterRuleList* list = filter_rule_list_create(); FilterRuleList* list = filter_rule_list_create();
@@ -112,11 +116,78 @@ static void test_filter_list_accepts_merge_modifiers() {
printf(" dir-merge rule '%s' errored: %s\n", drules[i].rule, err); printf(" dir-merge rule '%s' errored: %s\n", drules[i].rule, err);
EXPECT_TRUE(ok); EXPECT_TRUE(ok);
EXPECT_EQ_INT(list->dir_merge_count, 1); EXPECT_EQ_INT(list->dir_merge_count, 1);
EXPECT_EQ_STR(list->dir_merge_names[0], drules[i].want); EXPECT_EQ_STR(list->dir_merges[0].name, drules[i].want);
EXPECT_EQ_INT(list->dir_merges[0].no_prefixes, drules[i].no_prefixes);
EXPECT_EQ_INT(list->dir_merges[0].word_split, drules[i].word_split);
EXPECT_EQ_INT(list->dir_merges[0].no_inherit, drules[i].no_inherit);
EXPECT_EQ_INT(list->dir_merges[0].exclude_self, drules[i].exclude_self);
if (drules[i].exclude_self) {
EXPECT_EQ_INT(list->count, 1);
EXPECT_EQ_STR(list->items[0]->pattern, ".rules");
EXPECT_EQ_INT(list->items[0]->action, FILTER_ACTION_EXCLUDE);
} else {
EXPECT_EQ_INT(list->count, 0);
}
filter_rule_list_free(list); filter_rule_list_free(list);
} }
unlink(path); /* merge,n reads the file normally; no-inherit is meaningless for a single
* merge so the rule is not marked. */
{
FilterRuleList* list = filter_rule_list_create();
char err[256] = "";
char rule[600];
snprintf(rule, sizeof(rule), "merge,n %s", prefixed);
EXPECT_TRUE(filter_rule_list_parse_append(list, rule, NULL, NULL, err, sizeof(err)));
EXPECT_EQ_INT(list->count, 1);
EXPECT_EQ_STR(list->items[0]->pattern, "*.tmp");
EXPECT_FALSE(list->items[0]->no_inherit);
filter_rule_list_free(list);
}
/* merge,e adds an implicit exclude for the merge file's basename. */
{
FilterRuleList* list = filter_rule_list_create();
char err[256] = "";
char rule[600];
snprintf(rule, sizeof(rule), "merge,e %s", prefixed);
EXPECT_TRUE(filter_rule_list_parse_append(list, rule, NULL, NULL, err, sizeof(err)));
EXPECT_EQ_INT(list->count, 2);
EXPECT_EQ_STR(list->items[0]->pattern, "prefixed");
EXPECT_EQ_INT(list->items[0]->action, FILTER_ACTION_EXCLUDE);
EXPECT_EQ_STR(list->items[1]->pattern, "*.tmp");
filter_rule_list_free(list);
}
/* merge,- reads the file as bare exclude patterns with no prefix parsing. */
{
FilterRuleList* list = filter_rule_list_create();
char err[256] = "";
char rule[600];
snprintf(rule, sizeof(rule), "merge,- %s", bare);
EXPECT_TRUE(filter_rule_list_parse_append(list, rule, NULL, NULL, err, sizeof(err)));
EXPECT_EQ_INT(list->count, 2);
EXPECT_EQ_STR(list->items[0]->pattern, "*.log");
EXPECT_EQ_STR(list->items[1]->pattern, "*.tmp");
filter_rule_list_free(list);
}
/* merge,-w word-splits bare patterns on whitespace. */
{
FilterRuleList* list = filter_rule_list_create();
char err[256] = "";
char rule[600];
snprintf(rule, sizeof(rule), "merge,w- %s", words);
EXPECT_TRUE(filter_rule_list_parse_append(list, rule, NULL, NULL, err, sizeof(err)));
EXPECT_EQ_INT(list->count, 2);
EXPECT_EQ_STR(list->items[0]->pattern, "*.log");
EXPECT_EQ_STR(list->items[1]->pattern, "*.tmp");
filter_rule_list_free(list);
}
unlink(prefixed);
unlink(bare);
unlink(words);
rmdir(tmpl); rmdir(tmpl);
} }
@@ -283,6 +354,76 @@ static void test_filter_rules_apply_supported_modifiers() {
} }
} }
static FilterRule* chain_rule(const char* owner, const char* pattern, FilterAction action,
bool no_inherit) {
FilterRule* rule = calloc(1, sizeof(FilterRule));
if (!rule)
return NULL;
rule->action = action;
rule->sides = FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER;
rule->owner = str_dup(owner);
rule->pattern = str_dup(pattern);
rule->no_inherit = no_inherit;
if (!rule->owner || !rule->pattern) {
filter_rule_free(rule);
return NULL;
}
return rule;
}
/* The receiver's per-directory chain: a containing directory's rules win over
* an ancestor's (deepest-first), root rules are inherited, and a no-inherit
* rule applies only to its own directory's direct children. */
static void test_filter_dir_rules_chain(void) {
FilterRuleList* list = filter_rule_list_create();
EXPECT_NOT_NULL(list);
FilterRule* root_log = chain_rule("", "*.log", FILTER_ACTION_EXCLUDE, false);
FilterRule* sub_keep = chain_rule("sub", "keep.log", FILTER_ACTION_INCLUDE, false);
FilterRule* sub_tmp = chain_rule("sub", "*.tmp", FILTER_ACTION_EXCLUDE, true);
EXPECT_NOT_NULL(root_log);
EXPECT_NOT_NULL(sub_keep);
EXPECT_NOT_NULL(sub_tmp);
EXPECT_TRUE(filter_rule_list_add(list, root_log));
EXPECT_TRUE(filter_rule_list_add(list, sub_keep));
EXPECT_TRUE(filter_rule_list_add(list, sub_tmp));
/* Root rule inherited by every directory (leaf match). */
EXPECT_EQ_INT(filter_dir_rules_apply_side(list, "a.log", "a.log", false), FILTER_ACTION_PROTECT);
EXPECT_EQ_INT(filter_dir_rules_apply_side(list, "sub/a.log", "a.log", false),
FILTER_ACTION_PROTECT);
/* The deeper include overrides the inherited root exclude. */
EXPECT_EQ_INT(filter_dir_rules_apply_side(list, "sub/keep.log", "keep.log", false),
FILTER_ACTION_RISK);
/* No-inherit applies directly in its owner... */
EXPECT_EQ_INT(filter_dir_rules_apply_side(list, "sub/x.tmp", "x.tmp", false),
FILTER_ACTION_PROTECT);
/* ...but not below it. */
EXPECT_EQ_INT(filter_dir_rules_apply_side(list, "sub/deep/x.tmp", "x.tmp", false),
FILTER_ACTION_NONE);
/* No matching rule. */
EXPECT_EQ_INT(filter_dir_rules_apply_side(list, "sub/deep/plain.txt", "plain.txt", false),
FILTER_ACTION_NONE);
filter_rule_list_free(list);
}
static void test_filter_rule_clone_copies_every_field(void) {
char err[128] = "";
FilterRule* original = filter_rule_parse("-!p /a/*.o", NULL, err, sizeof(err));
EXPECT_NOT_NULL(original);
FilterRule* copy = filter_rule_clone(original);
EXPECT_NOT_NULL(copy);
EXPECT_TRUE(copy != original);
EXPECT_EQ_INT(copy->action, original->action);
EXPECT_EQ_INT(copy->sides, original->sides);
EXPECT_EQ_INT(copy->anchored, original->anchored);
EXPECT_EQ_INT(copy->dir_only, original->dir_only);
EXPECT_EQ_INT(copy->negate, original->negate);
EXPECT_EQ_INT(copy->perishable, original->perishable);
EXPECT_EQ_STR(copy->pattern, original->pattern);
filter_rule_free(original);
filter_rule_free(copy);
}
void test_filter() { void test_filter() {
test_filter_list_rejects_xattr_modifier(); test_filter_list_rejects_xattr_modifier();
test_filter_list_rejects_unsupported_modifiers(); test_filter_list_rejects_unsupported_modifiers();
@@ -291,4 +432,6 @@ void test_filter() {
test_filter_list_merge_file_still_supported(); test_filter_list_merge_file_still_supported();
test_filter_rule_parse_rejects_unsupported_and_keeps_supported(); test_filter_rule_parse_rejects_unsupported_and_keeps_supported();
test_filter_rules_apply_supported_modifiers(); test_filter_rules_apply_supported_modifiers();
test_filter_dir_rules_chain();
test_filter_rule_clone_copies_every_field();
} }
+7
View File
@@ -708,6 +708,7 @@ static void test_late_manifest_abort_frees_keepset() {
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
EXPECT_TRUE(send_status(p[1], STATUS_ABORT)); EXPECT_TRUE(send_status(p[1], STATUS_ABORT));
DeleteManifest* pending = NULL; DeleteManifest* pending = NULL;
@@ -733,6 +734,7 @@ static void test_late_manifest_eof_frees_keepset() {
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
shutdown(p[1], SHUT_WR); shutdown(p[1], SHUT_WR);
DeleteManifest* pending = NULL; DeleteManifest* pending = NULL;
@@ -758,12 +760,14 @@ static void test_late_second_manifest_frees_both() {
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST)); EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
EXPECT_TRUE(send_int(p[1], 1)); EXPECT_TRUE(send_int(p[1], 1));
EXPECT_TRUE(send_str(p[1], "second.txt")); EXPECT_TRUE(send_str(p[1], "second.txt"));
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* missing-args section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */ EXPECT_TRUE(send_int(p[1], 0)); /* synchronized-directories section is empty */
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
DeleteManifest* pending = NULL; DeleteManifest* pending = NULL;
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1); EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
@@ -799,6 +803,7 @@ static void test_receive_manifest_three_sections() {
EXPECT_TRUE(send_int(p[1], 2)); EXPECT_TRUE(send_int(p[1], 2));
EXPECT_TRUE(send_str(p[1], ".")); EXPECT_TRUE(send_str(p[1], "."));
EXPECT_TRUE(send_str(p[1], "dir")); EXPECT_TRUE(send_str(p[1], "dir"));
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
DeleteManifest* manifest = receive_manifest_entries(p[0]); DeleteManifest* manifest = receive_manifest_entries(p[0]);
EXPECT_NOT_NULL(manifest); EXPECT_NOT_NULL(manifest);
@@ -917,6 +922,7 @@ static void test_receiver_pending_commits_missing_args() {
EXPECT_TRUE(send_str(p[1], "gone.txt")); EXPECT_TRUE(send_str(p[1], "gone.txt"));
EXPECT_TRUE(send_str(p[1], "never_here.txt")); EXPECT_TRUE(send_str(p[1], "never_here.txt"));
EXPECT_TRUE(send_int(p[1], 0)); /* no synchronized directories */ EXPECT_TRUE(send_int(p[1], 0)); /* no synchronized directories */
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
EXPECT_TRUE(send_status(p[1], STATUS_FINISHED)); EXPECT_TRUE(send_status(p[1], STATUS_FINISHED));
/* NULL pending: the single-threaded commit path deletes at FINISHED. The /* NULL pending: the single-threaded commit path deletes at FINISHED. The
@@ -1309,6 +1315,7 @@ static void test_dry_run_delete_plan_commit_does_not_delete() {
EXPECT_TRUE(send_int(p[1], 0)); /* size-skipped prefixes */ EXPECT_TRUE(send_int(p[1], 0)); /* size-skipped prefixes */
EXPECT_TRUE(send_int(p[1], 1)); /* missing-args exact deletions */ EXPECT_TRUE(send_int(p[1], 1)); /* missing-args exact deletions */
EXPECT_TRUE(send_str(p[1], "victim.txt")); EXPECT_TRUE(send_str(p[1], "victim.txt"));
EXPECT_TRUE(send_int(p[1], 0)); /* per-directory filter-rule block is empty */
EXPECT_TRUE(send_int(p[1], 1)); /* apply: a real plan */ EXPECT_TRUE(send_int(p[1], 1)); /* apply: a real plan */
EXPECT_TRUE(send_str(p[1], ".")); /* receive root plan */ EXPECT_TRUE(send_str(p[1], ".")); /* receive root plan */
EXPECT_TRUE(send_int(p[1], 0)); /* kept child directories */ EXPECT_TRUE(send_int(p[1], 0)); /* kept child directories */
+2 -1
View File
@@ -349,8 +349,9 @@ static void test_walker_protect_rules_shield_dest_only() {
FilterRuleList* rules = filter_base_build(rule_text, 3, false, false, err, sizeof(err)); FilterRuleList* rules = filter_base_build(rule_text, 3, false, false, err, sizeof(err));
EXPECT_NOT_NULL(rules); EXPECT_NOT_NULL(rules);
size_t deleted = 0; size_t deleted = 0;
DeleteProtectRules protect = {.base_rules = rules, .dir_rules = NULL};
DeleteWalkResult result = DeleteWalkResult result =
delete_extras_limited(root, manifest, NULL, 100000, NULL, 0, rules, &deleted, NULL); delete_extras_limited(root, manifest, NULL, 100000, NULL, 0, &protect, &deleted, NULL);
EXPECT_EQ_INT((int)result, (int)DELETE_WALK_OK); EXPECT_EQ_INT((int)result, (int)DELETE_WALK_OK);
EXPECT_TRUE(file_exists(root, "keep.txt")); EXPECT_TRUE(file_exists(root, "keep.txt"));
EXPECT_FALSE(file_exists(root, "extra.log")); /* risk wins the first match */ EXPECT_FALSE(file_exists(root, "extra.log")); /* risk wins the first match */