Merge branch 'feat/parity-leftovers' into feat/parity-completion
# Conflicts: # src/client/scanner.c # src/client/scanner.h
This commit is contained in:
@@ -4963,15 +4963,20 @@ class TestFuzzy:
|
||||
"no-candidate fuzzy run should have sent the whole file"
|
||||
|
||||
def test_dissimilar_sibling_is_not_used(self, shared_server):
|
||||
# The destination holds a large sibling whose basename is too different
|
||||
# from the incoming name; the name gate must reject it and fall back to
|
||||
# a whole-file transfer.
|
||||
# A sibling whose basename is too different from the incoming name is
|
||||
# rejected by rsync's fuzzy distance window (the length gap exceeds
|
||||
# 25), so the run falls back to a whole-file transfer. A distinct
|
||||
# mtime keeps rsync's exact size+mtime first pass from accepting it.
|
||||
source, dest = self._prepare("dissim")
|
||||
old_bytes, new_bytes = _random_payloads()
|
||||
self._seed_dest(source, dest, {"totally-unrelated-notes.bin": old_bytes},
|
||||
long_name = "totally-unrelated-notes-with-a-very-long-name.bin"
|
||||
self._seed_dest(source, dest, {long_name: old_bytes},
|
||||
shared_server.port)
|
||||
with open(os.path.join(source, self.NEW_NAME), "wb") as fh:
|
||||
fh.write(new_bytes)
|
||||
received_dir = get_dest_received_dir(dest, source)
|
||||
os.utime(os.path.join(received_dir, long_name), (self.TS, self.TS))
|
||||
os.utime(os.path.join(source, self.NEW_NAME), (self.TS + 100000, self.TS + 100000))
|
||||
result, proxy = self._run_measured(source, dest, ["--fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy dissimilar-sibling run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -4980,6 +4985,28 @@ class TestFuzzy:
|
||||
assert proxy.client_to_server > len(new_bytes) // 2, \
|
||||
"a dissimilar-named sibling must not be used as a fuzzy basis"
|
||||
|
||||
def test_exact_size_mtime_sibling_is_used(self, shared_server):
|
||||
# rsync's fuzzy first pass accepts a sibling with an exact size+mtime
|
||||
# match regardless of how unrelated its name is (its content is almost
|
||||
# certainly the same).
|
||||
source, dest = self._prepare("exact")
|
||||
old_bytes, new_bytes = _random_payloads()
|
||||
self._seed_dest(source, dest, {"unrelated-blob.bin": old_bytes},
|
||||
shared_server.port)
|
||||
with open(os.path.join(source, self.NEW_NAME), "wb") as fh:
|
||||
fh.write(new_bytes)
|
||||
received_dir = get_dest_received_dir(dest, source)
|
||||
ts = 1600000000
|
||||
os.utime(os.path.join(received_dir, "unrelated-blob.bin"), (ts, ts))
|
||||
os.utime(os.path.join(source, self.NEW_NAME), (ts, ts))
|
||||
result, proxy = self._run_measured(source, dest, ["--fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy exact size+mtime run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == new_bytes
|
||||
assert proxy.client_to_server < len(new_bytes) // 4, \
|
||||
"an exact size+mtime sibling should be used as a fuzzy basis"
|
||||
|
||||
def test_fuzzy_helps_when_dest_holds_an_unsuitable_file(self, shared_server):
|
||||
# The destination DOES hold the exact new name, but it is a tiny stale
|
||||
# file (below the delta engine's minimum, ratio far outside its window),
|
||||
|
||||
+60
-10
@@ -1044,10 +1044,11 @@ static void test_parse_args_basis_dirs() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Absolute, escaping, or degenerate basis-dir values must be rejected up
|
||||
front: they would resolve outside the destination root on the receiver. */
|
||||
/* Escaping or degenerate basis-dir values must be rejected up front (they would
|
||||
resolve outside the destination root on the receiver); an absolute path is
|
||||
accepted (rsync parity) and canonicalized with its leading '/' preserved. */
|
||||
static void test_parse_args_basis_invalid_paths() {
|
||||
static const char* const invalid[] = {"/abs", "..", "a/../b", "."};
|
||||
static const char* const invalid[] = {"..", "a/../b", ".", "/", ""};
|
||||
for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--link-dest", (char*)invalid[i], "/src", "/dst"};
|
||||
@@ -1056,6 +1057,15 @@ static void test_parse_args_basis_invalid_paths() {
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--link-dest=/abs/dir", "/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->basis_count, 1);
|
||||
EXPECT_EQ_STR(cfg->basis_dirs[0].path, "/abs/dir");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Basis dirs require the per-file incremental handshake, which -s disables. */
|
||||
@@ -2551,15 +2561,35 @@ static void test_parse_args_filter_rules() {
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, missing_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
|
||||
/* rsync shorthands/modifiers we do not support are rejected instead of being
|
||||
* silently parsed as literal patterns. */
|
||||
static const char* const unsupported[] = {
|
||||
": .rsync-filter", ". /tmp/rules", "-s foo", "-p bar", "-C", "-! *.o", "!",
|
||||
/* Full rsync grammar (rule words, modifiers, clear) is supported. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* grammar_argv[] = {"fastsync",
|
||||
"--filter=hide *.tmp",
|
||||
"--filter=show *.txt",
|
||||
"--filter=protect *.bak",
|
||||
"--filter=risk *.o",
|
||||
"--filter=-s foo",
|
||||
"--filter=-p bar",
|
||||
"--filter=-! *.o",
|
||||
"--filter=dir-merge .rules",
|
||||
"--filter=!",
|
||||
"/src",
|
||||
"/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 11, grammar_argv, positional_args, &positional_count), 0);
|
||||
config_delete(cfg);
|
||||
|
||||
/* Genuinely malformed rules are still rejected. */
|
||||
static const char* const malformed[] = {
|
||||
"merge", /* merge requires a filename */
|
||||
"dir-merge", /* dir-merge requires a filename */
|
||||
"clear extra", /* clear takes no pattern */
|
||||
"no-such-rule x", /* unknown rule word */
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(unsupported) / sizeof(unsupported[0]); i++) {
|
||||
for (size_t i = 0; i < sizeof(malformed) / sizeof(malformed[0]); i++) {
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* rule_argv[] = {"fastsync", "--filter", (char*)unsupported[i], "/src", "/dst"};
|
||||
char* rule_argv[] = {"fastsync", "--filter", (char*)malformed[i], "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, rule_argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
@@ -3146,6 +3176,27 @@ static void test_parse_args_chown() {
|
||||
EXPECT_TRUE(cfg->chown_gid_set);
|
||||
EXPECT_EQ_INT(cfg->chown_gid, IDENTITY_CURRENT);
|
||||
config_delete(cfg);
|
||||
|
||||
/* A --chown NAME is converted to the equivalent receiver-resolved map rule
|
||||
* (rsync implements --chown as --usermap=*:USER --groupmap=*:GROUP), so the
|
||||
* name is carried on the wire as to_name instead of being resolved on the
|
||||
* sender. A name that does not exist on the sender is accepted and left for
|
||||
* the receiver to resolve (or warn about), matching rsync. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv5[] = {"fastsync", "--chown=no_such_user_zzz:no_such_group_zzz", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv5, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->usermap_count, 1);
|
||||
EXPECT_NOT_NULL(cfg->usermap[0].to_name);
|
||||
if (cfg->usermap[0].to_name)
|
||||
EXPECT_EQ_STR(cfg->usermap[0].to_name, "no_such_user_zzz");
|
||||
EXPECT_FALSE(cfg->chown_uid_set);
|
||||
EXPECT_EQ_INT(cfg->groupmap_count, 1);
|
||||
EXPECT_NOT_NULL(cfg->groupmap[0].to_name);
|
||||
if (cfg->groupmap[0].to_name)
|
||||
EXPECT_EQ_STR(cfg->groupmap[0].to_name, "no_such_group_zzz");
|
||||
EXPECT_FALSE(cfg->chown_gid_set);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --copy-as=USER[:GROUP] (P7 Wave E): resolve the user/group against the local
|
||||
@@ -3220,7 +3271,6 @@ static void test_parse_args_rejects_malformed_identity() {
|
||||
{"--groupmap", "@1"},
|
||||
{"--groupmap", "no_such_group_qqq:x"},
|
||||
{"--chown", "a:b:c"},
|
||||
{"--chown", "no_such_user_zzz:"},
|
||||
{"--copy-as", ""},
|
||||
{"--copy-as", ":"},
|
||||
{"--copy-as", "a:b:c"},
|
||||
|
||||
+10
-2
@@ -1192,7 +1192,10 @@ static void test_config_basis_wire_rejects_escaping() {
|
||||
c->basis_dirs = calloc(1, sizeof(BasisDest));
|
||||
c->basis_dirs[0].type = BASIS_DEST_LINK;
|
||||
c->basis_dirs[0].path = str_dup("/abs");
|
||||
EXPECT_FALSE(roundtrip_config_ok(c));
|
||||
/* An absolute basis dir is accepted (rsync parity); it is only usable when it
|
||||
lies within the receiver's authorized root, which file_open_secure_parent
|
||||
enforces at lookup time. */
|
||||
EXPECT_TRUE(roundtrip_config_ok(c));
|
||||
config_delete(c);
|
||||
|
||||
/* A well-formed list still round-trips even with a manually built struct. */
|
||||
@@ -1225,8 +1228,13 @@ static void test_config_basis_normalization() {
|
||||
/* Degenerate values that normalize away to nothing stay rejected. */
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "."), -1);
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, ".."), -1);
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "/abs"), -1);
|
||||
/* An absolute path is canonicalized (leading '/' preserved) and accepted. */
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "/abs"), 0);
|
||||
EXPECT_EQ_STR(c->basis_dirs[c->basis_count - 1].path, "/abs");
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "/a//b/"), 0);
|
||||
EXPECT_EQ_STR(c->basis_dirs[c->basis_count - 1].path, "/a/b");
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "a/../b"), -1);
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "/"), -1);
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, ""), -1);
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
@@ -855,7 +855,7 @@ static void test_filter_rules(bool parallel) {
|
||||
/* - *.tmp excludes only the tmp file; other files remain (default include). */
|
||||
const char* exclude_only[] = {"- *.tmp"};
|
||||
char err[160];
|
||||
FilterRuleList* base = filter_base_build(exclude_only, 1, false, err, sizeof(err));
|
||||
FilterRuleList* base = filter_base_build(exclude_only, 1, false, false, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(base);
|
||||
ScannerOptions options = {0};
|
||||
options.base_filters = base;
|
||||
@@ -875,7 +875,7 @@ static void test_filter_rules(bool parallel) {
|
||||
|
||||
/* Anchored include then exclude-all: only root-level keep* survives. */
|
||||
const char* anchored[] = {"+ /a.txt", "- *"};
|
||||
base = filter_base_build(anchored, 2, false, err, sizeof(err));
|
||||
base = filter_base_build(anchored, 2, false, false, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(base);
|
||||
options.base_filters = base;
|
||||
rc = parallel ? collect_files_parallel(root, &options, &paths, &count)
|
||||
@@ -889,7 +889,7 @@ static void test_filter_rules(bool parallel) {
|
||||
/* The common include idiom (the exact rule order the CLI compiles from
|
||||
* --include='*.txt' --exclude='*'): only .txt files survive. */
|
||||
const char* idiom[] = {"+ *.txt", "- *"};
|
||||
base = filter_base_build(idiom, 2, false, err, sizeof(err));
|
||||
base = filter_base_build(idiom, 2, false, false, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(base);
|
||||
options.base_filters = base;
|
||||
rc = parallel ? collect_files_parallel(root, &options, &paths, &count)
|
||||
@@ -905,7 +905,7 @@ static void test_filter_rules(bool parallel) {
|
||||
/* An include rule alone is NOT a mandatory whitelist (rsync semantics): only
|
||||
* the matching file is affected, everything else is still transferred. */
|
||||
const char* include_alone[] = {"+ *.txt"};
|
||||
base = filter_base_build(include_alone, 1, false, err, sizeof(err));
|
||||
base = filter_base_build(include_alone, 1, false, false, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(base);
|
||||
options.base_filters = base;
|
||||
rc = parallel ? collect_files_parallel(root, &options, &paths, &count)
|
||||
@@ -932,7 +932,7 @@ static void test_filter_dir_only_and_anchored(bool parallel) {
|
||||
|
||||
const char* rules[] = {"- /sub/"};
|
||||
char err[160];
|
||||
FilterRuleList* base = filter_base_build(rules, 1, false, err, sizeof(err));
|
||||
FilterRuleList* base = filter_base_build(rules, 1, false, false, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(base);
|
||||
ScannerOptions options = {0};
|
||||
options.base_filters = base;
|
||||
@@ -966,7 +966,7 @@ static void test_cvs_defaults(bool parallel) {
|
||||
create_test_file("test_scan_cvs/keep.txt", "keep");
|
||||
|
||||
char err[160];
|
||||
FilterRuleList* base = filter_base_build(NULL, 0, true, err, sizeof(err));
|
||||
FilterRuleList* base = filter_base_build(NULL, 0, true, false, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(base);
|
||||
ScannerOptions options = {0};
|
||||
options.base_filters = base;
|
||||
@@ -1005,6 +1005,7 @@ static void test_per_dir_filter(bool parallel) {
|
||||
|
||||
ScannerOptions options = {0};
|
||||
options.per_dir_filters = true;
|
||||
options.exclude_per_dir_filter_files = true; /* -FF */
|
||||
if (parallel)
|
||||
options.num_threads = 2;
|
||||
char** paths = NULL;
|
||||
@@ -1137,6 +1138,7 @@ static void test_per_dir_filter_override(bool parallel) {
|
||||
|
||||
ScannerOptions options = {0};
|
||||
options.per_dir_filters = true;
|
||||
options.exclude_per_dir_filter_files = true; /* -FF */
|
||||
if (parallel)
|
||||
options.num_threads = 2;
|
||||
char** paths = NULL;
|
||||
|
||||
Reference in New Issue
Block a user