fix(parity): rsync 3.4.1 symlink and special-node semantics (#287, #288)

#287:
- --safe-links: keep safe in-tree links AS symlinks and skip unsafe
  (absolute or ".."-escaping) ones, mirroring rsync's unsafe_symlink().
  Skipped links are recorded as delete-protected so --delete does not
  remove their destination mirror (no silent data loss).
- --copy-unsafe-links: preserve safe links as symlinks and dereference
  only unsafe ones.
- --munge-links: receiver-side rewrite storing /rsyncd-munged/-prefixed
  targets (rsync parity), replacing the no-op #SYMLINK sender prefix.
- -l: store the target verbatim, including absolute and ".." targets
  (rsync -l parity); the old receiver containment silently dropped them.

#288:
- --specials: recreate unix-domain sockets via mknod(S_IFSOCK), which
  Linux permits unprivileged; keep EEXIST/EPERM skip behavior.
- --copy-devices: copy a device's content into a regular file when
  requested; skip unrequested non-regular entries like rsync's default.
This commit is contained in:
2026-09-15 21:57:48 +02:00
parent 23552e823d
commit ea4ab661b4
8 changed files with 573 additions and 250 deletions
+21 -2
View File
@@ -818,10 +818,24 @@ static void test_special_socket_path_log_escaped() {
set_log_level(LOG_LEVEL_WARNING);
log_set_8_bit_output(false);
const char* root = "test_special_sock_escape_root";
const char* existing = "test_special_sock_escape_root/evil\npath";
unlink(existing);
rmdir(root);
EXPECT_EQ_INT(mkdir(root, 0700), 0);
FILE* planted = fopen(existing, "wb");
EXPECT_NOT_NULL(planted);
fclose(planted);
FILE* capture = tmpfile();
EXPECT_NOT_NULL(capture);
log_set_file(capture);
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
cfg->preserve_specials = true;
cfg->use_metadata = true;
File* file = file_create("evil\npath");
EXPECT_NOT_NULL(file);
file->is_special = true;
@@ -829,7 +843,9 @@ static void test_special_socket_path_log_escaped() {
EXPECT_NOT_NULL(file->metadata);
file->metadata->mode = S_IFSOCK | 0644;
FileSaveResult result = file_save_to_disk_full("/tmp/dst", file, NULL);
/* A non-matching entry already occupies the path: the socket creation is
refused and the warning must escape the path's control byte. */
FileSaveResult result = file_save_to_disk_full(root, file, cfg);
EXPECT_EQ_INT(result, FILE_SAVE_SKIPPED);
fflush(capture);
@@ -841,8 +857,11 @@ static void test_special_socket_path_log_escaped() {
log_set_file(NULL);
fclose(capture);
file_destroy(file);
config_delete(cfg);
unlink(existing);
rmdir(root);
EXPECT_NOT_NULL(strstr(output, "socket not recreated: evil\\#012path"));
EXPECT_NOT_NULL(strstr(output, "refusing to replace existing entry with socket: evil\\#012path"));
}
/* B1: a client-planted FIFO at the destination must not block the receiver's