feat(filter): per-directory merge rules at the receiver; implement e/n/w/- modifiers
This commit is contained in:
+14
-9
File diff suppressed because one or more lines are too long
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
+302
-87
@@ -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;
|
/* Merge files inside a per-directory file resolve relative to that
|
||||||
size_t line_cap = 0;
|
directory. */
|
||||||
bool ok = true;
|
bool ok =
|
||||||
while (true) {
|
filter_merge_read(list, fp, spec->name, spec, opts, dir_path, owner_rel, 0, err, err_size);
|
||||||
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
|
|
||||||
directory. */
|
|
||||||
if (!filter_list_parse_append_depth(list, p, opts, dir_path, 0, err, err_size)) {
|
|
||||||
ok = false;
|
|
||||||
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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user