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

This commit is contained in:
2026-09-23 22:46:14 +02:00
parent 29f8be161c
commit b414f197af
22 changed files with 1154 additions and 192 deletions
+24 -13
View File
@@ -32,6 +32,19 @@ static bool keep_is_file(const PathIndex* index, const char* rel_path) {
return path_index_contains(index, rel_path);
}
/* 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. */
DeleteEntryType delete_entry_type_of_mode(mode_t 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,
const PathIndex* dirs, DeleteWalkState* state,
const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules, bool parent_deletable,
const DeleteProtectRules* protect, bool parent_deletable,
bool* all_removed) {
DeleteDirEntry* entries = NULL;
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)) {
shielded[i] = true;
local_survives = true;
} else if (protect_rules &&
filter_rules_apply_side(protect_rules, child_rel, entries[i].name, entries[i].is_dir,
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT) {
} else if (delete_protect_verdict(protect, child_rel, entries[i].name, entries[i].is_dir) ==
FILTER_ACTION_PROTECT) {
/* A first-match protect rule shields the extra; for a directory the whole
subtree is shielded (rsync prunes an excluded directory), so do not
descend. */
@@ -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);
bool child_all_removed = false;
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))
operation_ok = false;
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);
bool child_all_removed = false;
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))
operation_ok = false;
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,
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)
*count_out = 0;
if (!manifest || !out)
@@ -474,7 +486,7 @@ bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
.observer = NULL,
.observer_context = NULL};
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)
ok = false;
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,
const ArrayList* synced_dirs, size_t max_delete,
const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules,
const DeleteProtectRules* protect,
size_t* deleted_out, size_t* skipped_out,
DeletePathObserver observer,
void* observer_context) {
@@ -527,7 +539,7 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
.observer = observer,
.observer_context = observer_context};
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)
ok = false;
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,
const ArrayList* synced_dirs, size_t max_delete,
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) {
return delete_extras_limited_observed(dest_root, manifest, synced_dirs, max_delete, skips,
skip_count, protect_rules, deleted_out, skipped_out, NULL,
NULL);
skip_count, protect, deleted_out, skipped_out, NULL, NULL);
}
bool delete_extras(const char* dest_root, const ArrayList* manifest) {
+23 -4
View File
@@ -3,6 +3,7 @@
#include "array_list.h"
#include "config.h"
#include "filter.h"
#include <stdbool.h>
#include <stddef.h>
#include <sys/stat.h>
@@ -29,6 +30,24 @@ typedef enum {
DELETE_WALK_ERROR
} 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
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
@@ -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,
const ArrayList* synced_dirs, size_t max_delete,
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);
/* 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
* 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
* directory, its whole subtree) in place, while RISK/NONE fall through to the
* ordinary skip-prefix/keep-set logic. */
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
const ArrayList* synced_dirs, size_t max_delete,
const DeleteSkipEntry* skips, int skip_count,
const FilterRuleList* protect_rules,
const DeleteProtectRules* protect,
size_t* deleted_out, size_t* skipped_out,
DeletePathObserver observer,
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. */
bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
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);
/* Build the delete walk's skip-prefix set from the config's --delay-updates
+15 -2
View File
@@ -19,6 +19,7 @@
#include "data.h"
#include "delay_updates.h"
#include "delete_commit.h"
#include "delete_plan.h"
#include "delta.h"
#include "file.h"
#include "format.h"
@@ -102,6 +103,13 @@ DeleteManifest* receive_manifest_entries(int fd) {
delete_manifest_free(manifest);
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;
}
@@ -112,6 +120,7 @@ void delete_manifest_free(DeleteManifest* manifest) {
array_list_delete(manifest->protected);
array_list_delete(manifest->missing);
array_list_delete(manifest->dirs);
filter_rule_list_free(manifest->per_dir_rules);
free(manifest);
}
@@ -160,9 +169,11 @@ static bool delete_extras_budgeted_observed(const Config* config, const DeleteMa
remaining = budget->max_delete - budget->deleted;
size_t deleted = 0;
size_t skipped = 0;
DeleteProtectRules protect = {.base_rules = config->protect_rules,
.dir_rules = manifest->per_dir_rules};
DeleteWalkResult result = delete_extras_limited_observed(
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);
budget->deleted += deleted;
budget->skipped += skipped;
@@ -386,8 +397,10 @@ bool manifest_would_delete_list(const Config* config, const DeleteManifest* mani
DeleteSkipSet skips;
if (!delete_skips_build(config, manifest->protected, NULL, true, &skips))
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,
skips.entries, skips.count, config->protect_rules, out, count_out);
skips.entries, skips.count, &protect, out, count_out);
delete_skips_free(&skips);
return ok;
}
+8
View File
@@ -4,6 +4,7 @@
#include "array_list.h"
#include "config.h"
#include "delete.h"
#include "filter.h"
#include <stdbool.h>
/* 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
alone, matching rsync's "delete only in synchronized directories". */
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;
void delete_manifest_free(DeleteManifest* manifest);
+177 -12
View File
@@ -48,6 +48,7 @@ struct DeletePlanSender {
const ArrayList* protected_prefixes;
const ArrayList* size_skipped;
const ArrayList* missing_args;
const FilterRuleList* per_dir_rules;
size_t entries;
/* 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
@@ -284,12 +285,14 @@ bool delete_plan_sender_empty(const DeletePlanSender* sender) {
}
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)
return;
sender->protected_prefixes = protected_prefixes;
sender->size_skipped = size_skipped;
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
@@ -320,6 +323,160 @@ static int send_str_section(int fd, const ArrayList* list) {
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) {
if (!send_status(fd, STATUS_DELETE_PLAN))
return -1;
@@ -328,7 +485,8 @@ static int send_plan_node(int fd, DeletePlanSender* sender, PlanNode* node) {
if (!sender->config_sent) {
if (send_str_section(fd, sender->protected_prefixes) != 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;
sender->config_sent = true;
}
@@ -355,7 +513,8 @@ static int send_config_only(int fd, DeletePlanSender* sender) {
return -1;
if (send_str_section(fd, sender->protected_prefixes) != 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;
sender->config_sent = true;
if (!send_int(fd, 0)) /* apply = false */
@@ -472,6 +631,10 @@ struct DeletePlanSession {
ArrayList* protected_prefixes;
ArrayList* size_skipped;
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;
DeletePathObserver observer;
void* observer_context;
@@ -532,6 +695,7 @@ void delete_plan_session_destroy(DeletePlanSession* session) {
array_list_delete(session->protected_prefixes);
array_list_delete(session->size_skipped);
array_list_delete(session->missing);
filter_rule_list_free(session->per_dir_rules);
array_list_delete(session->deferred);
free(session);
}
@@ -604,16 +768,18 @@ static int open_plan_dir(const Config* config, const char* dir) {
typedef struct {
DeleteSkipSet set;
/* Receiver-side delete-protection rules received on the config frame (NULL
when the sender sent none). Evaluated per extra so a protect/risk rule is
/* Receiver-side delete-protection rules. `base` is the config-frame
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
commit walker. */
const FilterRuleList* protect_rules;
DeleteProtectRules protect;
} PlanSkips;
static bool build_plan_skips(const Config* config, const DeletePlanSession* session,
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
convert an absolute under-root path to its root-relative form, unlike the
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 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 rule_protected =
skips->protect_rules &&
filter_rules_apply_side(skips->protect_rules, child_rel, entries[i].name, is_dir,
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT;
bool rule_protected = delete_protect_verdict(&skips->protect, child_rel, entries[i].name,
is_dir) == FILTER_ACTION_PROTECT;
if (in_keep_dirs || in_keep_files || rule_protected) {
shielded[i] = true;
local_survives = true;
@@ -903,7 +1067,8 @@ int delete_plan_session_receive(DeletePlanSession* session, const Config* config
if (has_config) {
if (session->config_seen || !read_section(fd, session->protected_prefixes, 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);
return -1;
}
+15 -1
View File
@@ -25,6 +25,19 @@
* --delete-missing-args exact deletions, the shared --max-delete budget and,
* 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 ---- */
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
* carrier frame when the scope allows no directory plan. */
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
* rsync's first generator directory), after transmitting the per-run config
* block on its own carrier frame. Returns -1 on I/O error. */
+302 -87
View File
@@ -11,6 +11,8 @@
/* Write a diagnostic message into the caller's optional buffer. */
#define filter_set_error utils_set_error
static bool set_rule_owner(FilterRule* rule, const char* owner);
/* ---- Ordered rule lists ---- */
void filter_rule_free(FilterRule* rule) {
@@ -21,6 +23,28 @@ void filter_rule_free(FilterRule* 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) {
return calloc(1, sizeof(FilterRuleList));
}
@@ -48,33 +72,76 @@ void filter_rule_list_free(FilterRuleList* list) {
for (int i = 0; i < list->count; i++)
filter_rule_free(list->items[i]);
for (int i = 0; i < list->dir_merge_count; i++)
free(list->dir_merge_names[i]);
free(list->dir_merge_names);
free(list->dir_merges[i].name);
free(list->dir_merges);
free(list->items);
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"
* and -F's .rsync-filter). Duplicate names are ignored. */
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')
return false;
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;
}
if (list->dir_merge_count == list->dir_merge_capacity) {
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)
return false;
list->dir_merge_names = grown;
list->dir_merges = grown;
list->dir_merge_capacity = new_cap;
}
char* dup = str_dup(name);
if (!dup)
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;
}
@@ -177,10 +244,11 @@ static bool is_unsupported_modifier_char(char c) {
return c == 'e' || c == 'n' || c == 'w';
}
/* Merge-file modifiers rsync accepts on merge/dir-merge rules: 'e', 'n', 'w'
* and '-' (do not transfer the merge file). */
/* Merge-file modifiers rsync accepts on merge/dir-merge rules: 'e' (exclude
* self), 'n' (no inherit), 'w' (word split), '-' (bare excludes) and '+'
* (bare includes). */
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
@@ -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. */
static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides,
bool* sides_explicit, bool* negate, bool* anchored_mod,
bool* perishable, bool* xattr, bool* cvs_inject,
const char** pat_start, size_t* pat_len, char* bad_mod) {
bool* perishable, bool* xattr, bool* cvs_inject, bool* no_prefixes,
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;
*sides = FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER;
*sides_explicit = false;
@@ -238,6 +308,11 @@ static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides,
*perishable = false;
*xattr = false;
*cvs_inject = false;
*no_prefixes = false;
*include_defaults = false;
*word_split = false;
*no_inherit = false;
*exclude_self = false;
*pat_start = NULL;
*pat_len = 0;
*bad_mod = '\0';
@@ -312,6 +387,21 @@ static bool parse_rule_syntax(const char* text, RuleKind* kind, unsigned* sides,
case 'C':
*cvs_inject = true;
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:
break;
}
@@ -347,11 +437,13 @@ FilterRule* filter_rule_parse(const char* line, const FilterParseOptions* opts,
RuleKind kind = RULE_KIND_UNKNOWN;
unsigned sides;
bool sides_explicit, negate, anchored_mod, perishable, xattr, cvs_inject;
bool no_prefixes, include_defaults, word_split, no_inherit, exclude_self;
const char* pat;
size_t pat_len;
char bad_mod;
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')
filter_set_error(err, err_size, "unsupported filter modifier '%c'", bad_mod);
else
@@ -522,16 +614,130 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
const FilterParseOptions* opts, const char* base_dir,
int depth, char* err, size_t err_size);
/* Read a 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). */
/* Append one merge-file token/line to `list`, honoring the merge rule's
* no-prefix/include mode. In no-prefix mode the token is a bare pattern whose
* 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,
const FilterParseOptions* opts, const char* base_dir, int depth,
char* err, size_t err_size) {
const FilterDirMerge* spec, const FilterParseOptions* opts,
const char* base_dir, int depth, char* err, size_t err_size) {
if (name[0] == '\0') {
filter_set_error(err, err_size, "merge requires a filename");
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 =
(base_dir && base_dir[0] && name[0] != '/') ? path_cat(base_dir, name) : str_dup(name);
if (!path) {
@@ -544,29 +750,7 @@ static bool filter_list_merge_file(FilterRuleList* list, const char* name,
free(path);
return false;
}
char* line = NULL;
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);
bool ok = filter_merge_read(list, fp, path, spec, opts, base_dir, NULL, depth, err, err_size);
fclose(fp);
free(path);
return ok;
@@ -590,11 +774,13 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
RuleKind kind = RULE_KIND_UNKNOWN;
unsigned sides;
bool sides_explicit, negate, anchored_mod, perishable, xattr, cvs_inject;
bool no_prefixes, include_defaults, word_split, no_inherit, exclude_self;
const char* pat;
size_t pat_len;
char bad_mod;
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')
filter_set_error(err, err_size, "unsupported filter modifier '%c': %s", bad_mod, p);
else
@@ -640,7 +826,15 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
}
memcpy(name, pat, pat_len);
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);
return ok;
}
@@ -656,7 +850,8 @@ static bool filter_list_parse_append_depth(FilterRuleList* list, const char* lin
}
memcpy(name, pat, pat_len);
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);
if (!ok) {
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 ---- */
/* 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,
* 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
@@ -724,20 +923,20 @@ static void filter_file_rollback(FilterRuleList* list, int rules_before, int dir
filter_rule_free(list->items[i]);
list->count = first;
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;
}
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) {
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) {
if (err && err_size > 0)
err[0] = '\0';
if (exists)
*exists = false;
if (!list)
if (!list || !spec || !spec->name)
return false;
char* filter_path = path_cat(dir_path, name);
char* filter_path = path_cat(dir_path, spec->name);
if (!filter_path) {
filter_set_error(err, err_size, "memory allocation failed");
return false;
@@ -748,7 +947,7 @@ bool filter_file_append(FilterRuleList* list, const char* dir_path, const char*
if (errno == ENOENT || errno == ENOTDIR)
return true;
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));
free(escaped_dir);
return true;
@@ -757,51 +956,25 @@ bool filter_file_append(FilterRuleList* list, const char* dir_path, const char*
*exists = true;
int rules_before = list->count;
int dir_merges_before = list->dir_merge_count;
char* line = NULL;
size_t line_cap = 0;
bool ok = true;
while (true) {
ssize_t n = utils_getdelim_bounded(fp, &line, &line_cap, '\n', UTILS_MAX_LINE_LEN);
if (n < 0) {
if (errno == EFBIG) {
filter_set_error(err, err_size, "line in %s exceeds %d bytes", name,
(int)UTILS_MAX_LINE_LEN);
} else {
filter_set_error(err, err_size, "error reading %s: %s", name, strerror(errno));
}
ok = false;
break;
}
if (n == 0)
break;
const char* p = line;
while (*p == ' ' || *p == '\t')
p++;
if (*p == '\0' || *p == '\n' || *p == '\r' || *p == '#')
continue;
/* Merge files inside a per-directory file resolve relative to that
directory. */
if (!filter_list_parse_append_depth(list, p, opts, dir_path, 0, err, err_size)) {
ok = false;
break;
}
}
free(line);
/* Merge files inside a per-directory file resolve relative to that
directory. */
bool ok =
filter_merge_read(list, fp, spec->name, spec, opts, dir_path, owner_rel, 0, err, err_size);
fclose(fp);
if (!ok) {
filter_file_rollback(list, rules_before, dir_merges_before);
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;
}
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,
const char* owner_rel, const FilterParseOptions* opts,
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')
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;
if (rule->dir_only && !is_dir)
matched = false;
@@ -884,3 +1061,41 @@ FilterAction filter_rules_apply_side(const FilterRuleList* list, const char* rel
}
return FILTER_ACTION_NONE;
}
FilterAction filter_dir_rules_apply_side(const FilterRuleList* dir_rules, const char* rel_path,
const char* leaf, bool is_dir) {
if (!dir_rules || !rel_path)
return FILTER_ACTION_NONE;
size_t len = strlen(rel_path);
if (len == 0)
return FILTER_ACTION_NONE;
/* The containing directory of rel_path is the prefix before its final '/'. */
size_t owner_len = 0;
for (size_t i = 0; i < len; i++) {
if (rel_path[i] == '/')
owner_len = i;
}
for (;;) {
for (int i = 0; i < dir_rules->count; i++) {
const FilterRule* rule = dir_rules->items[i];
size_t rule_owner_len = rule && rule->owner ? strlen(rule->owner) : 0;
if (rule_owner_len != owner_len)
continue;
if (owner_len != 0 && memcmp(rule->owner, rel_path, owner_len) != 0)
continue;
FilterAction action = rule_matches(rule, rel_path, leaf, is_dir, FILTER_SIDE_RECEIVER);
if (action != FILTER_ACTION_NONE)
return action;
}
if (owner_len == 0)
break;
/* Move to the parent directory: the last '/' before owner_len. */
size_t parent = 0;
for (size_t j = 0; j < owner_len; j++) {
if (rel_path[j] == '/')
parent = j;
}
owner_len = parent;
}
return FILTER_ACTION_NONE;
}
+56 -11
View File
@@ -25,11 +25,12 @@
* Modifiers: '/' absolute anchor, '!' negate match, 'C' inject CVS defaults,
* 's' sender side, 'r' receiver side, 'p' perishable. The rsync 'x'
* (xattr-name) modifier is not implemented and is rejected explicitly
* everywhere. The merge-file modifiers 'e' (exclude the merge file itself),
* 'n' (do not inherit the merge file), 'w' (word-split the merge file) and '-'
* (do not transfer the merge file) are accepted and consumed only on merge/
* dir-merge rules (rejected on every other rule, matching rsync); their
* semantics are not implemented and they are otherwise ignored.
* everywhere. The merge-only modifiers are accepted only on merge/dir-merge
* rules (rejected on every other rule, matching rsync): 'e' excludes the merge
* file itself, 'n' makes the merged rules non-inheriting (they apply only to
* the directory that holds the merge file), 'w' word-splits the merge file on
* 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
* the pattern to its owner directory.
*/
@@ -55,18 +56,32 @@ typedef struct {
bool dir_only; /* pattern had a trailing '/': matches directories only */
bool negate; /* '!' modifier: match succeeds when the pattern does not */
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* pattern; /* cleaned glob pattern (no leading '/', no trailing '/') */
} 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 {
FilterRule** items; /* owned array of rule pointers */
int count;
int capacity;
/* Per-directory merge-file basenames registered by "dir-merge NAME"/": NAME"
* or by -F (.rsync-filter). Owned strings; the scanner reads each name in
* every directory it traverses. */
char** dir_merge_names;
/* Per-directory merge-file registrations. Owned; the scanner reads each
* name in every directory it traverses. */
FilterDirMerge* dir_merges;
int dir_merge_count;
int dir_merge_capacity;
} FilterRuleList;
@@ -83,13 +98,23 @@ typedef struct {
FilterRule* filter_rule_parse(const char* line, const FilterParseOptions* opts, char* err,
size_t err_size);
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);
/* Append a fully-parsed rule (takes ownership). Returns false on OOM. */
bool filter_rule_list_add(FilterRuleList* list, FilterRule* rule);
/* Register a per-directory merge-file basename (idempotent). Returns false on
* OOM. Used by the scanner to read custom "dir-merge" files. */
/* Register a per-directory merge-file basename (idempotent, no modifiers).
* 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);
/* 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
* FILE"/". FILE" (splices the file's rules) and "dir-merge NAME"/": NAME"
* (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,
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. */
FilterRuleList* filter_file_read(const char* dir_path, const char* owner_rel, bool* exists,
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,
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
+3
View File
@@ -34,6 +34,7 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
context->synced_dirs = NULL;
context->plan_dirs = NULL;
context->missing_args = NULL;
context->per_dir_rules = NULL;
context->scan_had_io_error = false;
context->remove_source_files = NULL;
context->early_delete = false;
@@ -210,6 +211,8 @@ void pipeline_context_sender_destroy(PipelineContextSender* context) {
array_list_delete(context->plan_dirs);
if (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)
array_list_delete(context->remove_source_files);
if (context->dir_entries)
+6
View File
@@ -67,6 +67,12 @@ typedef struct {
them in the manifest frame's third section and the receiver deletes each as
an explicit request. */
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.
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