feat: implement -R/--relative, --no-implied-dirs, --dirs/-d, --mkpath
CI / lint (pull_request) Successful in 26s
CI / sanitizers (address) (pull_request) Failing after 41s
CI / sanitizers (undefined) (pull_request) Successful in 40s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Failing after 37s
CI / build-and-test (pull_request) Failing after 2m35s

RSYNC_COMPAT Phase-2 row M: path-list construction and destination
directory creation while preserving traversal safety.

- -R/--relative with --files-from: transmit each listed entry under its
  bare relative destination path (no source-root mirror). Files keep an
  absolute local read path plus a separate wire/dest path (File.send_path);
  manifest, incremental quick-check and change output follow the wire path,
  so --delete and --remove-source-files stay consistent. -R without
  --files-from is unchanged (full mirror).
- --no-implied-dirs: client-only, only meaningful with -R + --files-from.
  A listed file whose parent dir is not itself (or via an ancestor)
  explicitly listed cannot be placed; the run fails up front with a clear
  error. No effect otherwise.
- --dirs/-d + --old-dirs/--old-d aliases: -d <dir> transmits the source
  root as an explicit empty directory entry (STATUS_MKDIR frame); with
  --files-from listed dirs are created empty and listed files transferred,
  never descending. Works single-threaded, -m (sequential scanner in the
  -m scan thread) and chunk-serialization (per-file type marker).
  Directory entries appear in the delete manifest.
- --mkpath: new wire bool; server creates the destination root (and missing
  leading components under its authorized root) at connection start. A
  missing destination root is now rejected by default.
- Protocol bumped to 2.7.0 (mkpath wire field + STATUS_MKDIR + chunk type
  marker). All receive paths funnel through file_save_to_disk_full which
  creates directories via the secure confined mkdir engine; dir entries are
  excluded from --remove-source-files outcome acknowledgements on both ends.
- Unit coverage: CLI parse (relative/dirs aliases/mkpath/no-implied-dirs),
  config round-trip (relative + mkpath), scanner --dirs non-recursion and
  -R send_path (sequential + parallel), receiver dir-entry save.
- Integration coverage: TestRelativeFilesFrom, TestNoImpliedDirs, TestDirs,
  TestMkpath (single and -m).
- RSYNC_COMPAT: 4 rows move to Implemented (Summary 67/3/5/1/71 = 147).
This commit is contained in:
2026-09-06 15:28:32 +02:00
parent a196522010
commit c2cf231158
26 changed files with 1127 additions and 91 deletions
+41 -22
View File
@@ -547,34 +547,54 @@ static void test_parse_args_unknown_option() {
config_delete(cfg);
}
/* Directory aliases must report the unsupported directory-only behavior clearly. */
static void test_parse_args_rejects_dirs_aliases() {
static const char* const options[] = {"--dirs", "--old-dirs", "--old-d"};
/* -d/--dirs and the rsync --old-dirs/--old-d aliases all enable directory-only
* transfers (--dirs maps every spelling onto the same config field). */
static void test_parse_args_dirs_aliases() {
static const char* const options[] = {"--dirs", "-d", "--old-dirs", "--old-d"};
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
Config* cfg = config_create();
char* argv[] = {"fastsync", (char*)options[i], "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
FILE* log_file = tmpfile();
char log_buffer[256] = {0};
EXPECT_NOT_NULL(log_file);
log_set_file(log_file);
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
fflush(log_file);
rewind(log_file);
EXPECT_TRUE(fread(log_buffer, 1, sizeof(log_buffer) - 1, log_file) > 0);
EXPECT_TRUE(strstr(log_buffer, options[i]) != NULL);
EXPECT_TRUE(strstr(log_buffer, "directory-only transfer is not implemented") != NULL);
EXPECT_TRUE(strstr(log_buffer, "requires --dirs") == NULL);
log_set_file(NULL);
fclose(log_file);
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->dirs);
config_delete(cfg);
}
}
/* -R/--relative, --no-implied-dirs and --mkpath are plain boolean flags. */
static void test_parse_args_relative_no_implied_mkpath() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "-R", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->relative);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* long_argv[] = {"fastsync", "--relative", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, long_argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->relative);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* noimplied_argv[] = {"fastsync", "--no-implied-dirs", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, noimplied_argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->no_implied_dirs);
EXPECT_FALSE(cfg->relative);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* mkpath_argv[] = {"fastsync", "--mkpath", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, mkpath_argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->mkpath);
config_delete(cfg);
}
/* --del is recognized as the rsync alias, but its timing mode is not implemented. */
static void test_parse_args_delete_during_alias_unimplemented() {
static const char* const options[] = {"--del", "--delete-during"};
@@ -609,8 +629,6 @@ static void test_parse_args_rejects_unimplemented_options() {
"--delete-after",
"--max-delete",
"--prune-empty-dirs",
"-R",
"--relative",
"-e",
"--rsh",
"--rsync-path",
@@ -1522,7 +1540,8 @@ void test_client_cli() {
test_parse_args_max_alloc_sizes();
test_parse_args_rejects_invalid_max_alloc();
test_parse_args_unknown_option();
test_parse_args_rejects_dirs_aliases();
test_parse_args_dirs_aliases();
test_parse_args_relative_no_implied_mkpath();
test_parse_args_delete_during_alias_unimplemented();
test_parse_args_rejects_unimplemented_options();
test_parse_args_quiet();