fix: filter precedence, files-from errors, NUL/CRLF and filter-rule rejections

Address an independent c-review of the files-from/filter feature:

- .rsync-filter precedence now matches rsync: evaluate the innermost
  (current) directory's rules first, then ancestors, then the command-line
  base (--filter/-C), so a deeper file's '+' can re-include what a shallower
  '-' excluded (regression tests in both scan modes; single-thread and -m).
- --files-from: a listed entry missing on disk and an empty list are now hard
  errors surfaced pre-transfer in send_files, send_files_multithreaded,
  dry-run and --list-only; '.' (whole tree) and empty listed dirs stay valid.
- scanner_path_relative now handles a transfer root of / (previously the
  scanner aborted on children of /).
- Reject unsupported rsync filter syntax explicitly (no silent no-ops):
  +/- modifiers other than '/' (! C s r p x) and rules beginning with ':'/'.'
  /'!' (merge/dir-merge/list-clear shorthands). Docs updated.
- -0/--from0 NUL mode preserves entry bytes (no CR/LF trimming); only newline
  mode trims. Absolute-entry error message no longer includes the newline.
- --no-from0/--no-cvs-exclude registered as negatable booleans.
- RSYNC_COMPAT rows updated for the precedence, rejection list, NUL-mode
  detail and the documented O(entries x files) scalability bound of the
  allow-set (Summary unchanged: 62/3/5/1/76 = 147).
This commit is contained in:
2026-09-06 14:16:12 +02:00
parent f4c15a7b78
commit 7f2180ef90
11 changed files with 339 additions and 29 deletions
+11 -8
View File
@@ -39,16 +39,19 @@ static bool string_list_add(StringList* list, const char* text) {
/* Validate and normalize one entry. Returns:
* 1 -> added to `out`
* 0 -> blank entry, skip
* -1 -> invalid (message set in `err`) */
static int normalize_entry(const char* raw, size_t len, StringList* out, char* err,
size_t err_size) {
/* Trim the trailing newline/carriage-return from line mode. */
while (len > 0 && (raw[len - 1] == '\n' || raw[len - 1] == '\r'))
len--;
* -1 -> invalid (message set in `err`)
* `strip_line_endings` trims a trailing CR/LF (line mode only); NUL mode keeps
* the entry bytes verbatim so names ending in CR/LF survive. */
static int normalize_entry(const char* raw, size_t len, bool strip_line_endings, StringList* out,
char* err, size_t err_size) {
if (strip_line_endings) {
while (len > 0 && (raw[len - 1] == '\n' || raw[len - 1] == '\r'))
len--;
}
if (len == 0)
return 0;
if (raw[0] == '/') {
snprintf(err, err_size, "absolute path entries are not allowed: '%s'", raw);
snprintf(err, err_size, "absolute path entries are not allowed: '%.*s'", (int)len, raw);
return -1;
}
/* Reject NUL bytes inside a token defensively (NUL-delimited mode splits on
@@ -134,7 +137,7 @@ FileListSet* file_list_load(const char* path, bool null_separated, char* err, si
bool ok = true;
char delim = null_separated ? '\0' : '\n';
while (ok && (n = getdelim(&line, &line_cap, delim, fp)) != -1) {
int r = normalize_entry(line, (size_t)n, &raw, err, err_size);
int r = normalize_entry(line, (size_t)n, !null_separated, &raw, err, err_size);
if (r < 0) {
ok = false;
break;
+35 -4
View File
@@ -19,6 +19,13 @@ static bool rule_text_is_unsupported_word(const char* p, size_t len) {
return false;
}
/* rsync include/exclude rule modifiers we do NOT implement. A rule whose +/- is
* immediately followed by one of these is rejected instead of being silently
* parsed as a literal pattern. */
static bool is_unsupported_rule_modifier(char c) {
return c == '!' || c == 'C' || c == 's' || c == 'r' || c == 'p' || c == 'x';
}
FilterRule* filter_rule_parse(const char* line, char* err, size_t err_size) {
if (err && err_size > 0)
err[0] = '\0';
@@ -34,7 +41,6 @@ FilterRule* filter_rule_parse(const char* line, char* err, size_t err_size) {
while (len > 0 && (text[len - 1] == '\n' || text[len - 1] == '\r'))
text[--len] = '\0';
FilterAction action = FILTER_ACTION_EXCLUDE;
const char* p = text;
while (*p == ' ' || *p == '\t')
p++;
@@ -44,11 +50,35 @@ FilterRule* filter_rule_parse(const char* line, char* err, size_t err_size) {
return NULL;
}
FilterAction action = FILTER_ACTION_EXCLUDE;
if (*p == '+' || *p == '-') {
action = *p == '+' ? FILTER_ACTION_INCLUDE : FILTER_ACTION_EXCLUDE;
p++;
/* Accept the rsync word forms include/exclude. */
/* rsync attaches rule modifiers directly to the +/- (e.g. "-s foo"). Only
* the '/' anchor modifier is supported; anything else is a clear error
* rather than a silently-ignored literal. */
if (*p != ' ' && *p != '\t' && *p != '\0' && is_unsupported_rule_modifier(*p)) {
snprintf(err, err_size,
"filter rule modifier '%c' is not supported (only the '/' anchor after +/- "
"is implemented; put a space between +/- and the pattern)",
*p);
free(text);
return NULL;
}
while (*p == ' ' || *p == '\t')
p++;
} else {
/* ':' (dir-merge) and '.' (merge) are rsync filter-rule shorthands. At the
* start of a rule they mean "merge this file", so reject them instead of
* silently turning them into inert exclude patterns. */
if (*p == ':' || *p == '.' || *p == '!') {
snprintf(err, err_size,
"filter rule starting with '%c' is not supported (merge/dir-merge/list-clear "
"shorthands are not implemented; use +/- include/exclude rules)",
*p);
free(text);
return NULL;
}
const char* sp = p;
while (*sp != '\0' && *sp != ' ' && *sp != '\t')
sp++;
@@ -68,16 +98,17 @@ FilterRule* filter_rule_parse(const char* line, char* err, size_t err_size) {
action = FILTER_ACTION_EXCLUDE;
p = sp;
}
while (*p == ' ' || *p == '\t')
p++;
}
while (*p == ' ' || *p == '\t')
p++;
if (*p == '\0') {
snprintf(err, err_size, "filter rule has no pattern");
free(text);
return NULL;
}
/* A pattern beginning with '/' is anchored (either as "-/foo" or "- /foo"). */
bool anchored = false;
if (*p == '/') {
anchored = true;
+6 -2
View File
@@ -18,8 +18,12 @@
* contains a .rsync-filter file for per-directory rules)
* a trailing '/' makes the rule match directories only
*
* Unsupported rsync rule types (merge/dir-merge/hide/show/protect/risk/clear,
* rule modifiers other than '/') are rejected with a clear error.
* Rejected explicitly (no silent no-ops): the rsync merge/dir-merge/list-clear
* shorthands written as a rule that starts with ':' or '.' or '!', the
* merge/dir-merge/hide/show/protect/risk/clear words, and every include/exclude
* rule modifier other than '/' (! C s r p x). The pattern must be separated
* from +/- by a space (or a single '/' anchor), exactly like rsync's
* "-s foo"/"-p ..." modifier syntax is refused.
*/
typedef enum {