feat(output): rsync 3.4.1 selection and output parity (#291, #292)

#291:
- Compile --exclude/--include/--exclude-from/--include-from into the SAME
  ordered rule list as --filter/-f (first match wins), so the common
  `--include='*.txt' --exclude='*'` idiom and include-alone semantics match
  rsync. The legacy per-kind scanner arrays are no longer applied.
- -x/--one-file-system emits the cross-device mount-point directory entry
  (empty) instead of dropping it, in both the sequential and parallel scanners.
- Stop passing the legacy arrays to the scanner; document -f is --filter.

#292:
- New src/shared/format.c/.h: rsync "big_num" (comma-grouped integers) and
  decimal -h human sizes, %M/%t timestamp, and the STATUS_DEST_INFO codec.
- Receiver answers each STATUS_CHECK with a pre-transfer destination snapshot
  (new report_dest_info wire field + STATUS_DEST_INFO, PROTOCOL_VERSION
  2.23.0) so the sender can render true itemize columns.
- Itemize now emits rsync-correct update/type chars and c/s/t/p/o/g columns
  for files, dirs, symlinks and hard links, comparing size/time/perms/owner/
  group against the reported destination.
- --out-format gains %i %n %f %l %b %M %t %o %p %B %U %G %L; %f is the
  relative display path, %M the YYYY/MM/DD-HH:MM:SS form, %b the literal
  bytes sent.
- --list-only prints transfer-relative names, directory entries and ls-style
  grouped sizes.
- --stats prints rsync's multi-line block on stdout; -h uses decimal units.

Tests: unit tests for the filter ordering, format primitives, itemize
columns; integration + differential tests against real rsync 3.4.1 for
itemize/out-format/list-only/selection and -x. Golden wire len/hash and
protocol version strings updated for 2.23.0.
This commit is contained in:
2026-09-15 21:57:39 +02:00
parent 23552e823d
commit 84827ca617
28 changed files with 1529 additions and 300 deletions
+46 -4
View File
@@ -317,7 +317,7 @@ static void test_parse_args_protocol_accept_current() {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char* argv_equals[] = {"fastsync", "--source-dir", "/src",
"--dest-dir", "/dst", "--protocol=2.22.0"};
"--dest-dir", "/dst", "--protocol=2.23.0"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 6, argv_equals, positional_args, &positional_count), 0);
@@ -327,7 +327,7 @@ static void test_parse_args_protocol_accept_current() {
cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir",
"/dst", "--protocol", "2.22.0"};
"/dst", "--protocol", "2.23.0"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION);
@@ -338,8 +338,8 @@ static void test_parse_args_protocol_accept_current() {
* failure (parse_args simply stores it; validate_config rejects it up front). */
static void test_parse_args_protocol_rejects_other_versions() {
static const char* const bad_versions[] = {"2.17", "2.16", "2.15.0", "2.16.0", "2.17.0",
"2.18.0", "2.19.0", "2.20.0", "2.21.0", "216",
"31", "abc", ""};
"2.18.0", "2.19.0", "2.20.0", "2.21.0", "2.22.0",
"216", "31", "abc", ""};
for (size_t i = 0; i < sizeof(bad_versions) / sizeof(bad_versions[0]); i++) {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
@@ -3729,6 +3729,12 @@ static void test_parse_args_inline_equals_forms() {
EXPECT_EQ_STR(cfg->exclude_patterns[0], "*.log");
EXPECT_EQ_INT(cfg->include_count, 1);
EXPECT_EQ_STR(cfg->include_patterns[0], "*.txt");
/* The same patterns are compiled, in command-line order, into the shared
* ordered --filter rule list (rsync first-match-wins). */
EXPECT_NOT_NULL(cfg->filters);
EXPECT_EQ_INT(cfg->filters->size, 2);
EXPECT_EQ_STR((char*)cfg->filters->items[0], "- *.log");
EXPECT_EQ_STR((char*)cfg->filters->items[1], "+ *.txt");
config_delete(cfg);
const char* list_path = "cli_inline_patterns.txt";
@@ -3776,6 +3782,41 @@ static void test_parse_args_inline_equals_forms() {
config_delete(cfg);
}
/* --exclude/--include compile into the SAME ordered filter list as --filter, so
* rsync's first-match-wins semantics hold: the common `--include='*.txt'
* --exclude='*'` idiom keeps the .txt files and drops the rest, and an
* --include rule with no matching exclude is not a mandatory whitelist. */
static void test_parse_args_include_exclude_order() {
Config* cfg = config_create();
int positional_args[2];
int positional_count = 0;
char* argv[] = {"fastsync", "--include=*.txt", "--exclude=*", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 5, 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], "+ *.txt");
EXPECT_EQ_STR((char*)cfg->filters->items[1], "- *");
/* The order is reversible on the command line and the list follows it. */
Config* cfg2 = config_create();
positional_count = 0;
char* argv2[] = {"fastsync", "--exclude=*", "--include=*.txt", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg2, 5, argv2, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg2->filters->size, 2);
EXPECT_EQ_STR((char*)cfg2->filters->items[0], "- *");
EXPECT_EQ_STR((char*)cfg2->filters->items[1], "+ *.txt");
/* --filter and --exclude/--include interleave in command-line order. */
Config* cfg3 = config_create();
positional_count = 0;
char* argv3[] = {"fastsync", "--filter=- *.tmp", "--include=*.txt", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg3, 5, argv3, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg3->filters->size, 2);
EXPECT_EQ_STR((char*)cfg3->filters->items[0], "- *.tmp");
EXPECT_EQ_STR((char*)cfg3->filters->items[1], "+ *.txt");
config_delete(cfg);
config_delete(cfg2);
config_delete(cfg3);
}
/* OPT_NOOP compatibility flags (-s/--secluded-args, -r/--recursive) must never
* swallow the next argv: `fastsync -s SRC DST` keeps both positionals. */
static void test_parse_args_noop_does_not_consume_argv() {
@@ -4003,6 +4044,7 @@ void test_client_cli() {
test_parse_args_short_clustering();
test_parse_args_attached_short_values();
test_parse_args_inline_equals_forms();
test_parse_args_include_exclude_order();
test_parse_args_noop_does_not_consume_argv();
test_parse_args_backup_copy_links_shorts();
test_parse_args_rejects_unsupported_short();