feat(filter): per-directory merge rules at the receiver; implement e/n/w/- modifiers
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
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):
|
||||
for root in (rroot, froot):
|
||||
_mk(os.path.join(root, "extra1.txt"), b"e1\n", _OLD_MTIME)
|
||||
@@ -272,6 +305,50 @@ _CASES = [
|
||||
["-a", "--delete-after", "--filter=P *.log"],
|
||||
seed=seed_filter_protect, server_args=DELETE, ci=True,
|
||||
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 --------------------------------------------------
|
||||
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 os.path.exists(os.path.join(received, "extra.log"))
|
||||
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, 1)); /* missing args */
|
||||
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_wire_str(fd, "."));
|
||||
EXPECT_TRUE(send_int(fd, 0));
|
||||
@@ -265,10 +266,111 @@ static void test_config_only_frame_applies_missing_args(void) {
|
||||
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) {
|
||||
test_delete_delay_refilled_dir_removed_recursively();
|
||||
test_delete_delay_removed_file_counted();
|
||||
test_delete_delay_max_delete_bounds_actual();
|
||||
test_delete_delay_actual_removal_charges_budget();
|
||||
test_config_only_frame_applies_missing_args();
|
||||
test_filter_dir_rules_receive_bounds();
|
||||
}
|
||||
|
||||
+172
-29
@@ -1,6 +1,7 @@
|
||||
#include "test_filter.h"
|
||||
#include "filter.h"
|
||||
#include "test_utils.h"
|
||||
#include "utils.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.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.
|
||||
* 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() {
|
||||
char tmpl[] = "/tmp/fastsync_filter_mmod_XXXXXX";
|
||||
EXPECT_TRUE(mkdtemp(tmpl) != NULL);
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/rules", tmpl);
|
||||
FILE* fp = fopen(path, "w");
|
||||
char prefixed[512];
|
||||
char bare[512];
|
||||
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);
|
||||
fputs("- *.tmp\n", 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. */
|
||||
static const char* const fmts[] = {
|
||||
"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. */
|
||||
/* dir-merge with e/n/w/- registers the basename without the modifiers and
|
||||
* records the modifier flags; 'e' appends an exclude-self rule. */
|
||||
static const struct {
|
||||
const char* rule;
|
||||
const char* want;
|
||||
bool no_prefixes;
|
||||
bool word_split;
|
||||
bool no_inherit;
|
||||
bool exclude_self;
|
||||
} drules[] = {
|
||||
{"dir-merge,e .rules", ".rules"}, {"dir-merge,n .rules", ".rules"},
|
||||
{"dir-merge,w .rules", ".rules"}, {"dir-merge,- .rules", ".rules"},
|
||||
{":e .rules", ".rules"}, {":- .rules", ".rules"},
|
||||
{"dir-merge,e .rules", ".rules", false, false, false, true},
|
||||
{"dir-merge,n .rules", ".rules", false, false, true, false},
|
||||
{"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++) {
|
||||
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);
|
||||
EXPECT_TRUE(ok);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
test_filter_list_rejects_xattr_modifier();
|
||||
test_filter_list_rejects_unsupported_modifiers();
|
||||
@@ -291,4 +432,6 @@ void test_filter() {
|
||||
test_filter_list_merge_file_still_supported();
|
||||
test_filter_rule_parse_rejects_unsupported_and_keeps_supported();
|
||||
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)); /* missing-args 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));
|
||||
|
||||
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)); /* missing-args 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);
|
||||
|
||||
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)); /* missing-args 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_int(p[1], 1));
|
||||
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)); /* missing-args 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;
|
||||
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_str(p[1], "."));
|
||||
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]);
|
||||
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], "never_here.txt"));
|
||||
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));
|
||||
|
||||
/* 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], 1)); /* missing-args exact deletions */
|
||||
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_str(p[1], ".")); /* receive root plan */
|
||||
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));
|
||||
EXPECT_NOT_NULL(rules);
|
||||
size_t deleted = 0;
|
||||
DeleteProtectRules protect = {.base_rules = rules, .dir_rules = NULL};
|
||||
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_TRUE(file_exists(root, "keep.txt"));
|
||||
EXPECT_FALSE(file_exists(root, "extra.log")); /* risk wins the first match */
|
||||
|
||||
Reference in New Issue
Block a user