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:
@@ -473,6 +473,8 @@ static const NegatableOption NEGATABLE_OPTIONS[] = {
|
||||
{"sparse", "S", offsetof(Config, preserve_sparse)},
|
||||
{"inplace", NULL, offsetof(Config, inplace)},
|
||||
{"checksum", NULL, offsetof(Config, checksum)},
|
||||
{"from0", NULL, offsetof(Config, from0)},
|
||||
{"cvs-exclude", NULL, offsetof(Config, cvs_exclude)},
|
||||
|
||||
/* These options are also implied by --archive or handled outside the table. */
|
||||
{"compress", "c", offsetof(Config, use_compression)},
|
||||
|
||||
@@ -110,6 +110,46 @@ static void prepared_scanner_destroy(PreparedScanner* prepared) {
|
||||
prepared->base_filters = NULL;
|
||||
}
|
||||
|
||||
/* --files-from semantics: every listed entry must resolve under the source
|
||||
* root, otherwise rsync reports a hard error instead of silently transferring
|
||||
* nothing. An empty list is also an error. An entry of "." (the whole tree)
|
||||
* and listed-but-empty directories are valid. Runs before any transfer so the
|
||||
* failure is surfaced uniformly in the single-threaded, -m, dry-run and
|
||||
* --list-only paths. */
|
||||
static bool files_from_list_valid(const Config* config) {
|
||||
const FileListSet* set = (const FileListSet*)config->files_from_set;
|
||||
if (!set)
|
||||
return true;
|
||||
if (!config->send_directory) {
|
||||
log_message(LOG_LEVEL_ERROR, "--files-from requires a source directory");
|
||||
return false;
|
||||
}
|
||||
if (set->count == 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--files-from file '%s' contains no entries; nothing to transfer",
|
||||
config->files_from ? config->files_from : "");
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < set->count; i++) {
|
||||
const char* entry = set->entries[i];
|
||||
if (entry[0] == '\0')
|
||||
continue; /* "." == list the whole tree */
|
||||
char* full = path_cat(config->send_directory, entry);
|
||||
if (!full) {
|
||||
log_message(LOG_LEVEL_ERROR, "memory allocation failed while validating --files-from");
|
||||
return false;
|
||||
}
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--files-from entry '%s' not found in source '%s'", entry,
|
||||
config->send_directory);
|
||||
free(full);
|
||||
return false;
|
||||
}
|
||||
free(full);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Select the configured transport for both transfer execution paths. */
|
||||
static Client* connect_transfer_client(const Config* config) {
|
||||
if (config->transport == TRANSPORT_SSH) {
|
||||
@@ -307,6 +347,8 @@ static void pipeline_cancel(PipelineContextSender* context) {
|
||||
|
||||
/* Print dry-run manifest showing files that would be transferred. Returns 0 on success. */
|
||||
static int send_dry_run_manifest(const Config* config) {
|
||||
if (!files_from_list_valid(config))
|
||||
return -1;
|
||||
PreparedScanner prepared;
|
||||
if (!prepare_scanner(config, 0, &prepared))
|
||||
return -1;
|
||||
@@ -383,6 +425,8 @@ static int compare_list_entries(const void* left, const void* right) {
|
||||
* Directory lines are not printed because the scanner only yields regular
|
||||
* transfer candidates. Returns 0 on success, 1 on error. */
|
||||
static int send_list_only(const Config* config) {
|
||||
if (!files_from_list_valid(config))
|
||||
return 1;
|
||||
PreparedScanner prepared;
|
||||
if (!prepare_scanner(config, 0, &prepared))
|
||||
return 1;
|
||||
@@ -1002,6 +1046,8 @@ int send_files(Config* config) {
|
||||
return send_list_only(config);
|
||||
if (config->dry_run)
|
||||
return send_dry_run_manifest(config);
|
||||
if (!files_from_list_valid(config))
|
||||
return 1;
|
||||
|
||||
Client* client = connect_transfer_client(config);
|
||||
if (!client) {
|
||||
@@ -1139,6 +1185,8 @@ int send_files_multithreaded(Config** config_ptr) {
|
||||
return send_list_only(config);
|
||||
if (config->dry_run)
|
||||
return send_dry_run_manifest(config);
|
||||
if (!files_from_list_valid(config))
|
||||
return 1;
|
||||
|
||||
long pages = sysconf(_SC_AVPHYS_PAGES);
|
||||
long page_size = sysconf(_SC_PAGE_SIZE);
|
||||
|
||||
+22
-10
@@ -22,7 +22,9 @@ typedef struct {
|
||||
|
||||
/* A chain node: `own` holds the .rsync-filter rules of one directory, `parent`
|
||||
* the context that directory inherited (nearest ancestor with a filter file).
|
||||
* Rules are evaluated base-first, then from the outermost node inward. */
|
||||
* The chain for a directory's contents runs from that directory's own node up
|
||||
* to the root; the command-line base rules are evaluated after the whole
|
||||
* chain. */
|
||||
struct FilterNode {
|
||||
FilterNode* parent;
|
||||
FilterRuleList* own;
|
||||
@@ -46,15 +48,19 @@ static FilterNode* filter_node_alloc(FilterNode* parent, FilterRuleList* own) {
|
||||
return node;
|
||||
}
|
||||
|
||||
/* Evaluate a rule chain (base rules, then per-directory nodes outermost
|
||||
* first). Returns FILTER_ACTION_NONE when nothing matched. */
|
||||
/* Evaluate a rule chain for an entry inside the directory whose content
|
||||
* context is `node`. rsync precedence, highest first: the innermost (current)
|
||||
* directory's .rsync-filter rules, then each ancestor's, then the root's, and
|
||||
* finally the command-line base rules (--filter/-C). A deeper per-directory
|
||||
* file therefore overrides a shallower one, and per-directory files override
|
||||
* the base rules by default. Returns FILTER_ACTION_NONE when nothing matched. */
|
||||
static FilterAction chain_rules_apply(const FilterRuleList* base, const FilterNode* node,
|
||||
const char* rel, const char* leaf, bool is_dir) {
|
||||
if (node) {
|
||||
FilterAction parent_action = chain_rules_apply(base, node->parent, rel, leaf, is_dir);
|
||||
if (parent_action != FILTER_ACTION_NONE)
|
||||
return parent_action;
|
||||
return filter_rules_apply(node->own, rel, leaf, is_dir);
|
||||
FilterAction own_action = filter_rules_apply(node->own, rel, leaf, is_dir);
|
||||
if (own_action != FILTER_ACTION_NONE)
|
||||
return own_action;
|
||||
return chain_rules_apply(base, node->parent, rel, leaf, is_dir);
|
||||
}
|
||||
return base ? filter_rules_apply(base, rel, leaf, is_dir) : FILTER_ACTION_NONE;
|
||||
}
|
||||
@@ -117,13 +123,19 @@ bool scanner_same_filesystem(bool one_file_system, dev_t root_device, dev_t entr
|
||||
|
||||
/* Relative path of an on-disk path below `root`. The transfer root may be
|
||||
* given with a trailing slash; the returned rel path never has one and is ""
|
||||
* for the root itself. */
|
||||
static char* rel_for_fs_path(const char* root, const char* fs_path) {
|
||||
* for the root itself. A root of "/" is handled (its children start at "/").
|
||||
* Exposed so tests can exercise the mapping directly. */
|
||||
char* scanner_path_relative(const char* root, const char* fs_path) {
|
||||
size_t root_len = strlen(root);
|
||||
while (root_len > 1 && root[root_len - 1] == '/')
|
||||
root_len--;
|
||||
if (strncmp(root, fs_path, root_len) != 0)
|
||||
return NULL;
|
||||
if (root_len == 1 && root[0] == '/') {
|
||||
if (fs_path[1] == '\0')
|
||||
return str_dup("");
|
||||
return str_dup(fs_path + 1);
|
||||
}
|
||||
if (fs_path[root_len] == '\0')
|
||||
return str_dup("");
|
||||
if (fs_path[root_len] != '/')
|
||||
@@ -413,7 +425,7 @@ static int open_next_directory(DirectoryScanner* scanner) {
|
||||
free(de);
|
||||
|
||||
free(scanner->current_rel);
|
||||
scanner->current_rel = rel_for_fs_path(scanner->root_path, scanner->current_path);
|
||||
scanner->current_rel = scanner_path_relative(scanner->root_path, scanner->current_path);
|
||||
if (!scanner->current_rel) {
|
||||
log_message(LOG_LEVEL_ERROR, "Could not compute relative path under %s", scanner->root_path);
|
||||
scanner->failed = true;
|
||||
|
||||
@@ -111,6 +111,11 @@ void directory_scanner_destroy(DirectoryScanner* scanner);
|
||||
* the transfer root. Exposed so tests can exercise the rule directly. */
|
||||
bool scanner_same_filesystem(bool one_file_system, dev_t root_device, dev_t entry_device);
|
||||
|
||||
/* Relative path of an on-disk path below `root` ("" == the root itself, NULL
|
||||
* when `fs_path` is not under `root`). Handles trailing slashes and a root of
|
||||
* "/". Exposed so tests can exercise the mapping directly. */
|
||||
char* scanner_path_relative(const char* root, const char* fs_path);
|
||||
|
||||
ParallelScanner* parallel_scanner_create_with_options(const char* root_directory,
|
||||
const ScannerOptions* options,
|
||||
ProtocolSession* allocation_session);
|
||||
|
||||
Reference in New Issue
Block a user