fix: address c-review for -R/--dirs/--mkpath row

B1: destination-root existence/creation mishandled two legitimate forms.
file_directory_exists_secure/file_ensure_directory_secure now normalize a
trailing-slash destination (so the last component is never an empty leaf)
and treat a destination equal to the already-open authorized root as present
(no spurious <root>/<basename> nested dir with --mkpath). Confinement and
O_NOFOLLOW probing are unchanged. Regression integration tests: existing
dest with trailing slash works with and without --mkpath; dest == authorized
root works and creates no stray nested dir.

W1: chunk serialize/deserialize round-trip unit test for a directory entry
mixed with a regular file, with and without metadata; --dirs coverage under
-s and -s -m.
W2: --list-only and --dry-run now print file_wire_path() so all outputs show
the -R transformed relative name, matching -i/--out-format.
W3: RSYNC_COMPAT -d/--dirs note: dirs are created immediately under
--delay-updates (only regular files are staged).
W4: --dirs generator flushes chunks by element count too, so a long
--files-from list of empty directories cannot exceed the per-chunk file cap.
N1: STATUS_MKDIR added to status_to_string.
N2: removed dead TestMkpath._transfer.
N3: removed redundant --no-implied-dirs OPTION_TABLE row.
N4: integration tests: --dirs --delete keeps the just-created empty dir (and
deletes extras); a listed dir colliding with a regular file at the dest fails
cleanly.
This commit is contained in:
2026-09-06 16:10:00 +02:00
parent c2cf231158
commit 3275b6c978
8 changed files with 226 additions and 15 deletions
-1
View File
@@ -429,7 +429,6 @@ static const OptionEntry OPTION_TABLE[] = {
{"--old-dirs", NULL, OPT_FLAG, offsetof(Config, dirs)},
{"--old-d", NULL, OPT_FLAG, offsetof(Config, dirs)},
{"--relative", "-R", OPT_FLAG, offsetof(Config, relative)},
{"--no-implied-dirs", NULL, OPT_FLAG, offsetof(Config, no_implied_dirs)},
{"--mkpath", NULL, OPT_FLAG, offsetof(Config, mkpath)},
{"--delete-during", "--del", OPT_UNSUPPORTED, 0},
+3 -2
View File
@@ -435,7 +435,8 @@ static int send_dry_run_manifest(const Config* config) {
while ((chunk = directory_scanner_next(scanner)) != NULL) {
for (int i = 0; i < chunk->element_count; i++) {
if (!config->quiet) {
char* escaped_path = output_escape(chunk->items[i]->path, config->eight_bit_output);
char* escaped_path =
output_escape(file_wire_path(chunk->items[i]), config->eight_bit_output);
if (!escaped_path) {
chunk_destroy(chunk);
directory_scanner_destroy(scanner);
@@ -529,7 +530,7 @@ static int send_list_only(const Config* config) {
entries = grown;
capacity = new_capacity;
}
char* path = str_dup(f->path);
char* path = str_dup(file_wire_path(f));
if (!path) {
oom = true;
break;
+10
View File
@@ -465,6 +465,11 @@ static int open_next_directory(DirectoryScanner* scanner) {
listed regular files are transferred as files; nothing else is scanned, so
no descent into a listed directory can happen. */
/* Directory entries carry no payload, so the dirs generator also bounds every
chunk by element count; chunk_deserialize refuses more than this many files
per chunk (see MAX_FILES_PER_CHUNK in chunk.c). */
#define DIRS_CHUNK_MAX_FILES 65536U
/* Build the File for the transfer root directory itself (the `-d <dir>` and
* "." cases). */
static File* dirs_root_dir_file(DirectoryScanner* scanner) {
@@ -618,6 +623,11 @@ static Chunk* directory_scanner_next_dirs(DirectoryScanner* scanner) {
return NULL;
}
scanner->dirs_batch_size += file->data ? file->data->size : 0;
/* Empty directory entries carry no bytes, so a large --dirs --files-from
list must also be bounded by element count (the chunk deserializer caps
the number of files per chunk). */
if (scanner->dirs_batch->size >= (int)DIRS_CHUNK_MAX_FILES)
return dirs_flush_batch(scanner);
}
return dirs_flush_batch(scanner);
}