test: unit tests for basis-dir CLI parsing and config wire round-trip
- parse_args accepts each flag in both forms, keeps repetition order/types, implies --incremental + metadata, and rejects absolute/escaping/degenerate paths; validate_config rejects basis dirs combined with -s. - config wire round-trips a mixed basis list and rejects escaping/absolute paths on the receiver side.
This commit is contained in:
+83
-3
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user