Release v2.26.0 #284
@@ -480,6 +480,36 @@ static bool split_flag_level(const char* token, char* name, size_t name_size, in
|
||||
return true;
|
||||
}
|
||||
|
||||
/* rsync --debug/--info categories that FastSync accepts for CLI parity but has
|
||||
* no output wired to (yet). They must parse successfully so a valid rsync
|
||||
* invocation is not rejected up front; only categories with a FastSync
|
||||
* counterpart set a log flag. `pack`/`util` are FastSync-specific (packed
|
||||
* metadata / general utility logging). `syms`, `hl`, and `owner` are aliases
|
||||
* of rsync's `symsafe`, `hlink`, and `own`. */
|
||||
static bool is_accepted_debug_category(const char* name) {
|
||||
static const char* const categories[] = {
|
||||
"acl", "backup", "bind", "chdir", "cmd", "connect", "del", "deltasum",
|
||||
"dup", "exit", "filter", "flist", "fuzzy", "genr", "hash", "hl",
|
||||
"hlink", "iconv", "nstr", "own", "owner", "recv", "send", "time",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(categories) / sizeof(categories[0]); i++) {
|
||||
if (strcmp(name, categories[i]) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_accepted_info_category(const char* name) {
|
||||
static const char* const categories[] = {
|
||||
"backup", "del", "flist", "mount", "nonreg", "progress", "remove", "syms", "symsafe",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(categories) / sizeof(categories[0]); i++) {
|
||||
if (strcmp(name, categories[i]) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int parse_debug_flags(const char* value, Config* config) {
|
||||
if (!value || value[0] == '\0' || value[0] == ',' || value[strlen(value) - 1] == ',' ||
|
||||
strstr(value, ",,")) {
|
||||
@@ -522,6 +552,8 @@ static int parse_debug_flags(const char* value, Config* config) {
|
||||
flag = LOG_DEBUG_PACK;
|
||||
} else if (strcmp(name, "util") == 0) {
|
||||
flag = LOG_DEBUG_UTIL;
|
||||
} else if (is_accepted_debug_category(name)) {
|
||||
continue;
|
||||
} else {
|
||||
log_message(LOG_LEVEL_ERROR, "unsupported --debug flag: %s", token);
|
||||
free(flags);
|
||||
@@ -584,6 +616,8 @@ static int parse_info_flags(const char* value, Config* config) {
|
||||
flag = LOG_INFO_SKIP;
|
||||
else if (strcmp(name, "stats") == 0)
|
||||
flag = LOG_INFO_STATS;
|
||||
else if (is_accepted_info_category(name))
|
||||
continue;
|
||||
else {
|
||||
log_message(LOG_LEVEL_ERROR, "unsupported --info flag: %s", token);
|
||||
free(flags);
|
||||
|
||||
+9
-4
@@ -341,15 +341,20 @@ void print_usage(void) {
|
||||
}
|
||||
|
||||
void print_debug_usage(void) {
|
||||
printf("Supported debug flags: IO,PROTO,PACK,UTIL,ALL,NONE\n");
|
||||
printf("Emitting debug flags: IO,PROTO,PACK,UTIL,ALL,NONE\n");
|
||||
printf("Also accepted for rsync CLI parity (silent): ACL,BACKUP,BIND,CHDIR,\n");
|
||||
printf("CONNECT,CMD,DEL,DELTASUM,DUP,EXIT,FILTER,FLIST,FUZZY,GENR,HASH,HLINK,\n");
|
||||
printf("ICONV,NSTR,OWN,RECV,SEND,TIME.\n");
|
||||
printf("Flags may be comma-separated, for example: --debug=io,proto\n");
|
||||
printf("An optional level suffix is accepted (e.g. --debug=io2); level 0\n");
|
||||
printf("silences that item. Other rsync debug flags are unsupported and rejected.\n");
|
||||
printf("silences that item. Unknown names are rejected.\n");
|
||||
}
|
||||
|
||||
void print_info_usage(void) {
|
||||
printf("Supported info flags: COPY,NAME,MISC,SKIP,STATS,ALL,NONE\n");
|
||||
printf("Emitting info flags: COPY,NAME,MISC,SKIP,STATS,ALL,NONE\n");
|
||||
printf("Also accepted for rsync CLI parity (silent): BACKUP,DEL,FLIST,MOUNT,\n");
|
||||
printf("NONREG,PROGRESS,REMOVE,SYMSAFE.\n");
|
||||
printf("Flags may be comma-separated, for example: --info=name,stats\n");
|
||||
printf("An optional level suffix is accepted (e.g. --info=stats2); level 0\n");
|
||||
printf("silences that item. Other rsync info flags are unsupported and rejected.\n");
|
||||
printf("silences that item. Unknown names are rejected.\n");
|
||||
}
|
||||
|
||||
@@ -887,12 +887,33 @@ class TestFakeSuper:
|
||||
assert fh.read() == b"fake-super-data\n"
|
||||
|
||||
|
||||
class TestInfoDebugFlagParity:
|
||||
"""#01/#02: rsync's info/debug spellings are either mapped to real output or
|
||||
rejected by name (never silently ignored)."""
|
||||
# rsync 3.4.1's full --info/--debug vocabularies (from `rsync --info=help` /
|
||||
# `--debug=help`). FastSync must accept every one of them; only the categories
|
||||
# with an existing FastSync counterpart emit output, the rest are accepted but
|
||||
# currently silent.
|
||||
RSYNC_INFO_CATEGORIES = (
|
||||
"backup", "copy", "del", "flist", "misc", "mount", "name", "nonreg",
|
||||
"progress", "remove", "skip", "stats", "symsafe", "all", "none",
|
||||
)
|
||||
RSYNC_DEBUG_CATEGORIES = (
|
||||
"acl", "backup", "bind", "chdir", "connect", "cmd", "del", "deltasum",
|
||||
"dup", "exit", "filter", "flist", "fuzzy", "genr", "hash", "hlink",
|
||||
"iconv", "io", "nstr", "own", "proto", "recv", "send", "time", "all",
|
||||
"none",
|
||||
)
|
||||
# Extra categories FastSync also accepts: rsync's own help spells these
|
||||
# `symsafe`/`hlink`/`own`, but the historical aliases are kept working, and
|
||||
# `pack`/`util` are FastSync-specific debug channels.
|
||||
FASTSYNC_INFO_ALIASES = ("syms",)
|
||||
FASTSYNC_DEBUG_ALIASES = ("hl", "owner", "pack", "util")
|
||||
|
||||
@requires_rsync
|
||||
def test_mapped_info_categories_accepted_like_rsync(self, shared_server):
|
||||
|
||||
class TestInfoDebugFlagParity:
|
||||
"""#01/#02: FastSync accepts rsync 3.4.1's full --info/--debug vocabulary
|
||||
(with level suffixes) so a valid rsync invocation is never rejected up
|
||||
front. Unknown names are still refused by name."""
|
||||
|
||||
def _tree(self):
|
||||
source = os.path.join(TEST_DATA_DIR, "qw_flags_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "qw_flags_dst")
|
||||
rdst = os.path.join(TEST_DATA_DIR, "qw_flags_rdst")
|
||||
@@ -901,54 +922,78 @@ class TestInfoDebugFlagParity:
|
||||
clean_dir(rdst)
|
||||
with open(os.path.join(source, "a.txt"), "wb") as fh:
|
||||
fh.write(b"a\n")
|
||||
for cat in ("stats2", "name", "copy", "misc", "skip", "STATS2"):
|
||||
assert _rsync(["-a", "--info=" + cat, source + "/", rdst + "/"]).returncode == 0
|
||||
return source, dest, rdst
|
||||
|
||||
@requires_rsync
|
||||
@pytest.mark.ci
|
||||
def test_info_vocabulary_accepted_like_rsync(self, shared_server):
|
||||
source, dest, rdst = self._tree()
|
||||
for cat in RSYNC_INFO_CATEGORIES:
|
||||
rs = _rsync(["-a", "--info=" + cat, source + "/", rdst + "/"])
|
||||
assert rs.returncode == 0, f"rsync rejected --info={cat}: {rs.stderr}"
|
||||
clean_dir(rdst)
|
||||
result, _ = run_client(source, dest, flags=["--info=" + cat],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"--info={cat} must be accepted: {result.stderr[:200]}"
|
||||
)
|
||||
|
||||
@requires_rsync
|
||||
def test_mapped_debug_categories_accepted_like_rsync(self, shared_server):
|
||||
source = os.path.join(TEST_DATA_DIR, "qw_dflags_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "qw_dflags_dst")
|
||||
rdst = os.path.join(TEST_DATA_DIR, "qw_dflags_rdst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
clean_dir(rdst)
|
||||
with open(os.path.join(source, "a.txt"), "wb") as fh:
|
||||
fh.write(b"a\n")
|
||||
for cat in ("io2", "proto0", "all"):
|
||||
assert _rsync(["-a", "--debug=" + cat, source + "/", rdst + "/"]).returncode == 0
|
||||
clean_dir(rdst)
|
||||
result, _ = run_client(source, dest, flags=["--debug=" + cat],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"--debug={cat} must be accepted: {result.stderr[:200]}"
|
||||
f"--info={cat} must be accepted like rsync: {result.stderr[:200]}"
|
||||
)
|
||||
|
||||
@requires_rsync
|
||||
@pytest.mark.ci
|
||||
def test_unmapped_categories_rejected_by_name(self, shared_server):
|
||||
"""rsync accepts del/filter; fastsync has no mapping so it must refuse
|
||||
loudly, naming the category, rather than silently ignoring it."""
|
||||
source = os.path.join(TEST_DATA_DIR, "qw_umap_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "qw_umap_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
with open(os.path.join(source, "a.txt"), "wb") as fh:
|
||||
fh.write(b"a\n")
|
||||
# rsync accepts these (so they are valid rsync invocations).
|
||||
assert _rsync(["-a", "--info=del", source + "/", dest + "/"]).returncode == 0
|
||||
assert _rsync(["-a", "--debug=filter", source + "/", dest + "/"]).returncode == 0
|
||||
for flag, name in (("--info=del", "del"), ("--debug=filter", "filter")):
|
||||
def test_debug_vocabulary_accepted_like_rsync(self, shared_server):
|
||||
source, dest, rdst = self._tree()
|
||||
for cat in RSYNC_DEBUG_CATEGORIES:
|
||||
rs = _rsync(["-a", "--debug=" + cat, source + "/", rdst + "/"])
|
||||
assert rs.returncode == 0, f"rsync rejected --debug={cat}: {rs.stderr}"
|
||||
clean_dir(rdst)
|
||||
result, _ = run_client(source, dest, flags=["--debug=" + cat],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"--debug={cat} must be accepted like rsync: {result.stderr[:200]}"
|
||||
)
|
||||
|
||||
@requires_rsync
|
||||
@pytest.mark.ci
|
||||
def test_level_suffixes_accepted_like_rsync(self, shared_server):
|
||||
source, dest, rdst = self._tree()
|
||||
for flag in ("--info=stats2", "--info=copy0", "--info=all0",
|
||||
"--debug=io2", "--debug=proto0", "--debug=all4"):
|
||||
rs = _rsync(["-a", flag, source + "/", rdst + "/"])
|
||||
assert rs.returncode == 0, f"rsync rejected {flag}: {rs.stderr}"
|
||||
clean_dir(rdst)
|
||||
result, _ = run_client(source, dest, flags=[flag],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"{flag} must be accepted like rsync: {result.stderr[:200]}"
|
||||
)
|
||||
|
||||
def test_fastsync_alias_categories_accepted(self, shared_server):
|
||||
"""FastSync-specific/alias spellings: accepted (rsync spells them
|
||||
symsafe/hlink/own) but not emitted."""
|
||||
source, dest, _ = self._tree()
|
||||
for flag in (["--info=" + c for c in FASTSYNC_INFO_ALIASES] +
|
||||
["--debug=" + c for c in FASTSYNC_DEBUG_ALIASES]):
|
||||
result, _ = run_client(source, dest, flags=[flag],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"{flag} must be accepted: {result.stderr[:200]}"
|
||||
)
|
||||
|
||||
@requires_rsync
|
||||
@pytest.mark.ci
|
||||
def test_unknown_categories_rejected_by_name(self, shared_server):
|
||||
"""Truly unknown names are refused by name, exactly like rsync."""
|
||||
source, dest, rdst = self._tree()
|
||||
for flag, name in (("--info=bogus", "bogus"),
|
||||
("--debug=bogus", "bogus")):
|
||||
rs = _rsync(["-a", flag, source + "/", rdst + "/"])
|
||||
assert rs.returncode != 0, f"rsync unexpectedly accepted {flag}"
|
||||
result, _ = run_client(source, dest, flags=[flag],
|
||||
port=shared_server.port)
|
||||
assert result.returncode != 0, f"{flag} must be rejected"
|
||||
assert name in (result.stderr or ""), \
|
||||
assert name in (result.stderr or ""), (
|
||||
f"{flag} must be rejected by name, got: {result.stderr[:200]}"
|
||||
)
|
||||
|
||||
|
||||
class TestStopAtParity:
|
||||
|
||||
+20
-1
@@ -776,7 +776,7 @@ static void test_parse_args_debug_help() {
|
||||
}
|
||||
|
||||
static void test_parse_args_debug_flags_validation() {
|
||||
static const char* const values[] = {"", "io,", ",io", "io,,proto", "acl", "tls", "unknown"};
|
||||
static const char* const values[] = {"", "io,", ",io", "io,,proto", "tls", "unknown"};
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char option[64];
|
||||
@@ -1345,6 +1345,24 @@ static void test_parse_args_info_name_and_help() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* rsync 3.4.1's remaining --info/--debug categories parse successfully but
|
||||
* have no FastSync output wired to them, so they must not set any log flag. */
|
||||
static void test_parse_args_rsync_flag_vocabulary_accepted() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--info=backup,del,flist,mount,nonreg,progress,remove,symsafe,syms",
|
||||
"--debug=acl,backup,bind,chdir,cmd,connect,del,deltasum,dup,exit,"
|
||||
"filter,flist,fuzzy,genr,hash,hlink,iconv,nstr,own,recv,send,time,"
|
||||
"hl,owner",
|
||||
"/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->info_level, 0);
|
||||
EXPECT_EQ_INT(cfg->debug_level, 0);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test parse_args with --archive flag */
|
||||
/* rsync accepts a trailing level digit on --debug/--info items (e.g. io2,
|
||||
* all4); level 0 silences the item. */
|
||||
@@ -4441,6 +4459,7 @@ void test_client_cli() {
|
||||
test_parse_args_debug_flags();
|
||||
test_parse_args_debug_help();
|
||||
test_parse_args_debug_flags_validation();
|
||||
test_parse_args_rsync_flag_vocabulary_accepted();
|
||||
test_parse_args_debug_info_levels();
|
||||
test_parse_args_modify_window();
|
||||
test_parse_args_rejects_invalid_modify_window();
|
||||
|
||||
Reference in New Issue
Block a user