feat: add --files-from/-0, --filter, -C and -F filter layer
CI / lint (pull_request) Successful in 23s
CI / sanitizers (address) (pull_request) Successful in 40s
CI / sanitizers (undefined) (pull_request) Successful in 39s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 34s
CI / build-and-test (pull_request) Successful in 1m28s
CI / valgrind (pull_request) Successful in 36s
CI / lint (pull_request) Successful in 23s
CI / sanitizers (address) (pull_request) Successful in 40s
CI / sanitizers (undefined) (pull_request) Successful in 39s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 34s
CI / build-and-test (pull_request) Successful in 1m28s
CI / valgrind (pull_request) Successful in 36s
Implement the RSYNC_COMPAT Phase-2 filter/parser feature group: - --files-from=FILE (repeatable) plus -0/--from0 NUL delimiters: parse the source file list relative to the source root into a shared read-only allow-set; the scanner transfers listed files and the whole subtree of listed directories and prunes everything else in single- and multithreaded mode. Absolute/'..' entries and missing files are hard CLI errors. - --filter=RULE: rsync-style +/- rules (anchored '/', dir-only trailing '/', word include/exclude forms) evaluated first-match-wins with a default of include, as an independent layer from legacy --exclude/--include. Unsupported directives (merge/hide/... ) are rejected explicitly. -f stays sendfile. - -C/--cvs-exclude: well-known rsync CVS default exclude set. - -F: per-directory .rsync-filter files read during traversal and applied to the owning directory's subtree (single + parallel), never transferred. - Delete manifest still derives from what was actually sent. Client-only config fields; no wire/protocol change. Adds unit coverage (CLI parse, allow-set and filter scanning single+parallel) and integration tests (TestFilesFrom, TestFilters). RSYNC_COMPAT matrix rows updated: 5 rows move to Implemented (Summary 62/3/5/1/76 = 147).
This commit is contained in:
+135
-3
@@ -2,6 +2,7 @@
|
||||
#include "client_validation.h"
|
||||
#include "chmod.h"
|
||||
#include "config.h"
|
||||
#include "file_list.h"
|
||||
#include "log.h"
|
||||
#include "test_utils.h"
|
||||
#include "utils.h"
|
||||
@@ -607,9 +608,6 @@ static void test_parse_args_rejects_unimplemented_options() {
|
||||
"--delete-excluded",
|
||||
"--delete-after",
|
||||
"--max-delete",
|
||||
"--filter",
|
||||
"--files-from",
|
||||
"--cvs-exclude",
|
||||
"--prune-empty-dirs",
|
||||
"-R",
|
||||
"--relative",
|
||||
@@ -1265,6 +1263,137 @@ static void test_parse_args_log_file_format() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --filter is repeatable and accepts both "--filter RULE" and "--filter=RULE". */
|
||||
static void test_parse_args_filter_rules() {
|
||||
Config* cfg = config_create();
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
char* argv[] = {"fastsync", "--filter", "- *.tmp", "--filter=+ /keep.txt", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 6, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_NOT_NULL(cfg->filters);
|
||||
EXPECT_EQ_INT(cfg->filters->size, 2);
|
||||
EXPECT_EQ_STR((char*)cfg->filters->items[0], "- *.tmp");
|
||||
EXPECT_EQ_STR((char*)cfg->filters->items[1], "+ /keep.txt");
|
||||
config_delete(cfg);
|
||||
|
||||
/* An unsupported rsync rule type is rejected with a clear error. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* bad_argv[] = {"fastsync", "--filter=merge /tmp/excludes", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, bad_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
|
||||
/* A trailing --filter with no rule is a missing-argument error. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* missing_argv[] = {"fastsync", "/src", "/dst", "--filter"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, missing_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* -0/--from0, -C/--cvs-exclude and -F wire into their config flags. */
|
||||
static void test_parse_args_from0_cvs_filter_file_flags() {
|
||||
static const struct {
|
||||
const char* arg;
|
||||
bool from0;
|
||||
bool cvs;
|
||||
bool per_dir;
|
||||
} cases[] = {
|
||||
{"--from0", true, false, false},
|
||||
{"-0", true, false, false},
|
||||
{"--cvs-exclude", false, true, false},
|
||||
{"-C", false, true, false},
|
||||
{"-F", false, false, true},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", (char*)cases[i].arg, "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->from0, cases[i].from0);
|
||||
EXPECT_EQ_INT(cfg->cvs_exclude, cases[i].cvs);
|
||||
EXPECT_EQ_INT(cfg->per_dir_filter, cases[i].per_dir);
|
||||
config_delete(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
static void write_file_bytes(const char* path, const char* bytes, size_t len) {
|
||||
FILE* fp = fopen(path, "wb");
|
||||
EXPECT_NOT_NULL(fp);
|
||||
EXPECT_EQ_INT((int)fwrite(bytes, 1, len, fp), (int)len);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* --files-from is validated and parsed after the full argument scan, so -0 may
|
||||
* appear before or after it. */
|
||||
static void test_parse_args_files_from() {
|
||||
const char* list_path = "cli_files_from_list.txt";
|
||||
write_file_bytes(list_path, "a.txt\nsub/b.bin\n\n./c.txt\n", 25);
|
||||
Config* cfg = config_create();
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
char* argv[] = {"fastsync", "--files-from", (char*)list_path, "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->files_from, list_path);
|
||||
EXPECT_NOT_NULL(cfg->files_from_set);
|
||||
FileListSet* set = (FileListSet*)cfg->files_from_set;
|
||||
EXPECT_TRUE(file_list_affects(set, "a.txt"));
|
||||
EXPECT_TRUE(file_list_affects(set, "sub/b.bin"));
|
||||
EXPECT_TRUE(file_list_affects(set, "sub/b.bin/x"));
|
||||
EXPECT_TRUE(file_list_affects(set, "sub"));
|
||||
EXPECT_TRUE(file_list_affects(set, "c.txt"));
|
||||
EXPECT_FALSE(file_list_affects(set, "other.txt"));
|
||||
config_delete(cfg);
|
||||
remove(list_path);
|
||||
|
||||
/* -0 switches the separator to NUL regardless of argument order. */
|
||||
write_file_bytes(list_path, "x.txt\0y/z.bin\0", 14);
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* nul_argv[] = {"fastsync",
|
||||
"--files-from="
|
||||
"cli_files_from_list.txt",
|
||||
"-0", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, nul_argv, positional_args, &positional_count), 0);
|
||||
set = (FileListSet*)cfg->files_from_set;
|
||||
EXPECT_NOT_NULL(set);
|
||||
EXPECT_TRUE(file_list_affects(set, "x.txt"));
|
||||
EXPECT_TRUE(file_list_affects(set, "y/z.bin"));
|
||||
EXPECT_TRUE(file_list_affects(set, "y"));
|
||||
EXPECT_FALSE(file_list_affects(set, "z.txt"));
|
||||
config_delete(cfg);
|
||||
remove(list_path);
|
||||
|
||||
/* A missing list file is a hard parse-time error. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* missing_argv[] = {"fastsync", "--files-from", "does_not_exist_ff.txt", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, missing_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
|
||||
/* Absolute and traversal entries are rejected. */
|
||||
write_file_bytes(list_path, "/abs/path\n", 10);
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* abs_argv[] = {"fastsync",
|
||||
"--files-from="
|
||||
"cli_files_from_list.txt",
|
||||
"/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, abs_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
write_file_bytes(list_path, "../escape\n", 10);
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* trav_argv[] = {"fastsync",
|
||||
"--files-from="
|
||||
"cli_files_from_list.txt",
|
||||
"/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, trav_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
remove(list_path);
|
||||
}
|
||||
|
||||
void test_client_cli() {
|
||||
test_validate_config_required_paths();
|
||||
test_validate_config_incompatible_options();
|
||||
@@ -1344,4 +1473,7 @@ void test_client_cli() {
|
||||
test_parse_args_checksum_choice_aliases();
|
||||
test_parse_args_checksum_choice_requires_value();
|
||||
test_parse_args_temp_dir();
|
||||
test_parse_args_filter_rules();
|
||||
test_parse_args_from0_cvs_filter_file_flags();
|
||||
test_parse_args_files_from();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user