diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 425d8aa..47da3f8 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -595,6 +595,86 @@ static void test_parse_args_relative_no_implied_mkpath() { config_delete(cfg); } +/* Parse --compare-dest/--copy-dest/--link-dest, including the =value and + separate-argument forms, and verify the ordered (repeatable) basis list. */ +static void test_parse_args_basis_dirs() { + Config* cfg = config_create(); + int positional_args[2]; + int positional_count = 0; + char* argv[] = {"fastsync", "--link-dest=prior", "/src", "/dst"}; + EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); + EXPECT_TRUE(config_has_basis(cfg)); + EXPECT_EQ_INT(cfg->basis_count, 1); + EXPECT_EQ_INT(cfg->basis_dirs[0].type, BASIS_DEST_LINK); + EXPECT_EQ_STR(cfg->basis_dirs[0].path, "prior"); + /* Basis dirs are honored by the receiver-side per-file check, so they imply + --incremental (and, unless disabled, metadata) on the sender. */ + EXPECT_TRUE(cfg->use_incremental); + EXPECT_TRUE(cfg->use_metadata); + config_delete(cfg); + + cfg = config_create(); + positional_count = 0; + char* argv2[] = {"fastsync", "--compare-dest", "cmp", "/src", "/dst"}; + EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), 0); + EXPECT_EQ_INT(cfg->basis_count, 1); + EXPECT_EQ_INT(cfg->basis_dirs[0].type, BASIS_DEST_COMPARE); + EXPECT_EQ_STR(cfg->basis_dirs[0].path, "cmp"); + config_delete(cfg); + + /* Repetition is supported: entries keep command-line order and type. */ + cfg = config_create(); + positional_count = 0; + char* argv3[] = {"fastsync", "--link-dest=a", "--compare-dest=b", + "--link-dest=c", "--copy-dest=d", "/src", + "/dst"}; + EXPECT_EQ_INT(parse_args(cfg, 7, argv3, positional_args, &positional_count), 0); + EXPECT_EQ_INT(cfg->basis_count, 4); + EXPECT_EQ_INT(cfg->basis_dirs[0].type, BASIS_DEST_LINK); + EXPECT_EQ_STR(cfg->basis_dirs[0].path, "a"); + EXPECT_EQ_INT(cfg->basis_dirs[1].type, BASIS_DEST_COMPARE); + EXPECT_EQ_STR(cfg->basis_dirs[1].path, "b"); + EXPECT_EQ_INT(cfg->basis_dirs[2].type, BASIS_DEST_LINK); + EXPECT_EQ_STR(cfg->basis_dirs[2].path, "c"); + EXPECT_EQ_INT(cfg->basis_dirs[3].type, BASIS_DEST_COPY); + EXPECT_EQ_STR(cfg->basis_dirs[3].path, "d"); + config_delete(cfg); + + /* Nested relative basis dirs are allowed (they resolve below the root). */ + cfg = config_create(); + positional_count = 0; + char* argv4[] = {"fastsync", "--copy-dest=snap/2026-01", "/src", "/dst"}; + EXPECT_EQ_INT(parse_args(cfg, 4, argv4, positional_args, &positional_count), 0); + EXPECT_EQ_INT(cfg->basis_count, 1); + EXPECT_EQ_STR(cfg->basis_dirs[0].path, "snap/2026-01"); + config_delete(cfg); +} + +/* Absolute, escaping, or degenerate basis-dir values must be rejected up + front: they would resolve outside the destination root on the receiver. */ +static void test_parse_args_basis_invalid_paths() { + static const char* const invalid[] = {"/abs", "..", "a/../b", "."}; + for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--link-dest", (char*)invalid[i], "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1); + config_delete(cfg); + } +} + +/* Basis dirs require the per-file incremental handshake, which -s disables. */ +static void test_validate_config_basis_rejects_chunk_serialization() { + Config* cfg = valid_client_config(); + EXPECT_EQ_INT(config_basis_append(cfg, BASIS_DEST_LINK, "prior"), 0); + cfg->use_chunk_serialization = true; + EXPECT_FALSE(validate_config(cfg)); + cfg->use_chunk_serialization = false; + EXPECT_TRUE(validate_config(cfg)); + 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"}; @@ -632,9 +712,6 @@ static void test_parse_args_rejects_unimplemented_options() { "-e", "--rsh", "--rsync-path", - "--compare-dest", - "--copy-dest", - "--link-dest", "--delete-before", "--address", "--bind-address", @@ -1589,4 +1666,7 @@ void test_client_cli() { test_parse_args_files_from(); test_parse_args_filter_rules(); test_parse_args_from0_cvs_filter_file_flags(); + test_parse_args_basis_dirs(); + test_parse_args_basis_invalid_paths(); + test_validate_config_basis_rejects_chunk_serialization(); } diff --git a/tests/test_config.c b/tests/test_config.c index a89b498..15cdb10 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -438,6 +438,96 @@ static void test_config_delay_updates_reserved_backup_rejected() { config_delete(c); } +/* Basis-dir lists survive the config wire: each entry's type and path must + round-trip unchanged. */ +static void test_config_basis_roundtrip() { + if (is_running_under_valgrind()) + return; + Config* send_cfg = config_create(); + EXPECT_NOT_NULL(send_cfg); + send_cfg->send_directory = str_dup("/send/src"); + send_cfg->receive_root_directory = str_dup("/send/dst"); + EXPECT_EQ_INT(config_basis_append(send_cfg, BASIS_DEST_LINK, "prior"), 0); + EXPECT_EQ_INT(config_basis_append(send_cfg, BASIS_DEST_COMPARE, "snap/2026-01"), 0); + EXPECT_EQ_INT(config_basis_append(send_cfg, BASIS_DEST_COPY, "copy"), 0); + + int p[2]; + EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + close(p[1]); + io_set_fds(p[0], p[0]); + Config* recv = config_receive(p[0]); + bool ok = recv != NULL && recv->basis_count == 3 && recv->basis_dirs != NULL; + if (ok) { + ok = recv->basis_dirs[0].type == BASIS_DEST_LINK && + strcmp(recv->basis_dirs[0].path, "prior") == 0; + ok = ok && recv->basis_dirs[1].type == BASIS_DEST_COMPARE && + strcmp(recv->basis_dirs[1].path, "snap/2026-01") == 0; + ok = ok && recv->basis_dirs[2].type == BASIS_DEST_COPY && + strcmp(recv->basis_dirs[2].path, "copy") == 0; + } + config_delete(recv); + close(p[0]); + close(p[1]); + _exit(ok ? 0 : 1); + } else { + close(p[0]); + io_set_fds(p[1], p[1]); + bool sent = config_send(p[1], send_cfg); + int status; + waitpid(pid, &status, 0); + close(p[1]); + config_delete(send_cfg); + EXPECT_TRUE(sent); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +/* The receiver must reject a basis-dir path that would escape the destination + root. The values are injected directly (bypassing the client-side append + validator) so the receiver-side wire validation is what is exercised. */ +static void test_config_basis_wire_rejects_escaping() { + if (is_running_under_valgrind()) + return; + Config* c = config_create(); + EXPECT_NOT_NULL(c); + c->send_directory = str_dup("/src"); + c->receive_root_directory = str_dup("/dst"); + c->basis_count = 1; + c->basis_dirs = calloc(1, sizeof(BasisDest)); + c->basis_dirs[0].type = BASIS_DEST_LINK; + c->basis_dirs[0].path = str_dup("../../etc"); + EXPECT_FALSE(roundtrip_config_ok(c)); + config_delete(c); + + c = config_create(); + EXPECT_NOT_NULL(c); + c->send_directory = str_dup("/src"); + c->receive_root_directory = str_dup("/dst"); + c->basis_count = 1; + c->basis_dirs = calloc(1, sizeof(BasisDest)); + c->basis_dirs[0].type = BASIS_DEST_LINK; + c->basis_dirs[0].path = str_dup("/abs"); + EXPECT_FALSE(roundtrip_config_ok(c)); + config_delete(c); + + /* A well-formed list still round-trips even with a manually built struct. */ + c = config_create(); + EXPECT_NOT_NULL(c); + c->send_directory = str_dup("/src"); + c->receive_root_directory = str_dup("/dst"); + c->basis_count = 1; + c->basis_dirs = calloc(1, sizeof(BasisDest)); + c->basis_dirs[0].type = BASIS_DEST_COPY; + c->basis_dirs[0].path = str_dup("safe"); + EXPECT_TRUE(roundtrip_config_ok(c)); + config_delete(c); +} + static void test_config_is_remote_dest() { /* Valid SSH-style destinations */ EXPECT_TRUE(config_is_remote_dest("user@host:/path")); @@ -474,6 +564,8 @@ void test_config() { test_config_string_null_vs_empty_roundtrip(); test_config_temp_dir_roundtrip(); test_config_delay_updates_reserved_backup_rejected(); + test_config_basis_roundtrip(); + test_config_basis_wire_rejects_escaping(); } test_config_is_remote_dest(); }