- CLI: --append/--append-verify acceptance (parse + imply --incremental, validate) and incompatibility rejection with -s and --whole-file; both removed from the unimplemented reject list. - Config: on-the-wire append/append_verify round-trip. - Unit: append_resume_eligible / append_tail_length pure resume math. - Integration (test_append.py): matching-prefix resume is byte-identical and tail-only (wire bytes << source size); --append with a wrong prefix keeps prefix+tail (rsync parity) while --append-verify detects the mismatch and falls back to a byte-exact full transfer; --append with --inplace and -m.
825 lines
27 KiB
C
825 lines
27 KiB
C
#include "test_config.h"
|
|
#include "config.h"
|
|
#include "multiprocessing.h"
|
|
#include "protocol.h"
|
|
#include "queue.h"
|
|
#include "test_utils.h"
|
|
#include "utils.h"
|
|
#include <stdlib.h>
|
|
#include <sys/socket.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
static Config* make_config(const char* version, const char* src, const char* dst, bool save,
|
|
bool mt, bool cs, bool comp, bool meta, int clevel, bool sf,
|
|
unsigned long long csize) {
|
|
Config* cfg = config_create();
|
|
if (!cfg)
|
|
return NULL;
|
|
free(cfg->version);
|
|
cfg->version = str_dup(version);
|
|
cfg->send_directory = str_dup(src);
|
|
cfg->receive_root_directory = str_dup(dst);
|
|
cfg->save_to_disk = save;
|
|
cfg->use_multithreading = mt;
|
|
cfg->use_chunk_serialization = cs;
|
|
cfg->use_compression = comp;
|
|
cfg->use_metadata = meta;
|
|
cfg->compression_level = clevel;
|
|
cfg->use_sendfile = sf;
|
|
if (csize > 0)
|
|
cfg->chunk_size = csize;
|
|
return cfg;
|
|
}
|
|
|
|
static void test_config_lifecycle() {
|
|
Config* cfg = make_config("1.0", "/src", "/dst", true, true, false, false, false, 1, false, 0);
|
|
EXPECT_NOT_NULL(cfg);
|
|
EXPECT_EQ_STR(cfg->version, "1.0");
|
|
EXPECT_EQ_STR(cfg->send_directory, "/src");
|
|
EXPECT_EQ_STR(cfg->receive_root_directory, "/dst");
|
|
EXPECT_TRUE(cfg->save_to_disk);
|
|
EXPECT_TRUE(cfg->use_multithreading);
|
|
EXPECT_FALSE(cfg->use_chunk_serialization);
|
|
EXPECT_FALSE(cfg->use_compression);
|
|
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
|
|
EXPECT_NULL(cfg->ssh_destination);
|
|
config_delete(cfg);
|
|
}
|
|
|
|
static void test_config_ssh_dest() {
|
|
Config* cfg =
|
|
make_config("1.0", "/src", "user@host:/dst", true, false, false, false, false, 1, false, 0);
|
|
EXPECT_NOT_NULL(cfg);
|
|
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
|
|
EXPECT_NULL(cfg->ssh_destination);
|
|
EXPECT_EQ_STR(cfg->receive_root_directory, "user@host:/dst");
|
|
|
|
config_parse_ssh_dest(cfg);
|
|
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
|
|
EXPECT_EQ_STR(cfg->ssh_destination, "user@host:/dst");
|
|
EXPECT_EQ_STR(cfg->receive_root_directory, "/dst");
|
|
config_delete(cfg);
|
|
}
|
|
|
|
static void test_config_ssh_dest_local_path() {
|
|
Config* cfg =
|
|
make_config("1.0", "/src", "/local/path", true, false, false, false, false, 1, false, 0);
|
|
config_parse_ssh_dest(cfg);
|
|
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
|
|
EXPECT_NULL(cfg->ssh_destination);
|
|
EXPECT_EQ_STR(cfg->receive_root_directory, "/local/path");
|
|
config_delete(cfg);
|
|
}
|
|
|
|
static void test_config_ssh_dest_no_user() {
|
|
Config* cfg =
|
|
make_config("1.0", "/src", "host:/remote", true, false, false, false, false, 1, false, 0);
|
|
config_parse_ssh_dest(cfg);
|
|
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
|
|
EXPECT_EQ_STR(cfg->ssh_destination, "host:/remote");
|
|
EXPECT_EQ_STR(cfg->receive_root_directory, "/remote");
|
|
config_delete(cfg);
|
|
}
|
|
|
|
static void test_pipeline_sender_lifecycle() {
|
|
Config* cfg = make_config("2.0", "/src2", "/dst2", false, false, true, true, false, 1, false, 0);
|
|
Queue* q1 = queue_create(5, NULL);
|
|
Queue* q2 = queue_create(15, NULL);
|
|
|
|
PipelineContextSender* pcs = pipeline_context_sender_create(cfg, q1, q2);
|
|
EXPECT_NOT_NULL(pcs);
|
|
EXPECT_EQ_STR(pcs->config->version, "2.0");
|
|
EXPECT_EQ_INT(pcs->queue_scanner->capacity, 5);
|
|
EXPECT_EQ_INT(pcs->queue_loader->capacity, 15);
|
|
EXPECT_FALSE(pcs->scanner_done);
|
|
EXPECT_FALSE(pcs->loader_done);
|
|
EXPECT_EQ_INT((int)pcs->allocation_session.max_alloc, (int)cfg->max_alloc);
|
|
|
|
pipeline_context_sender_destroy(pcs);
|
|
}
|
|
|
|
static void test_pipeline_receiver_lifecycle() {
|
|
Config* cfg = make_config("3.0", "/src3", "/dst3", true, true, true, true, false, 1, false, 0);
|
|
Queue* q = queue_create(20, NULL);
|
|
|
|
PipelineContextReceiver* pcr = pipeline_context_receiver_create(cfg, q, 42, NULL);
|
|
EXPECT_NOT_NULL(pcr);
|
|
EXPECT_EQ_STR(pcr->config->version, "3.0");
|
|
EXPECT_EQ_INT(pcr->queue->capacity, 20);
|
|
EXPECT_EQ_INT(pcr->file_descriptor, 42);
|
|
EXPECT_FALSE(pcr->receiver_done);
|
|
|
|
pipeline_context_receiver_destroy(pcr);
|
|
}
|
|
|
|
static void test_config_send_receive() {
|
|
/* Create a config to send */
|
|
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");
|
|
send_cfg->save_to_disk = true;
|
|
send_cfg->use_multithreading = true;
|
|
send_cfg->use_chunk_serialization = false;
|
|
send_cfg->use_compression = true;
|
|
send_cfg->use_metadata = true;
|
|
send_cfg->use_executability = true;
|
|
send_cfg->use_delta = true;
|
|
send_cfg->whole_file = true;
|
|
send_cfg->fuzzy = true;
|
|
send_cfg->ignore_times = true;
|
|
send_cfg->size_only = true;
|
|
send_cfg->compression_level = 5;
|
|
send_cfg->chunk_size = 1024;
|
|
send_cfg->eight_bit_output = true;
|
|
send_cfg->modify_window = 4;
|
|
send_cfg->existing = true;
|
|
send_cfg->ignore_existing = true;
|
|
send_cfg->delay_updates = true;
|
|
send_cfg->relative = true;
|
|
send_cfg->mkpath = true;
|
|
send_cfg->skip_compress_set = true;
|
|
send_cfg->skip_compress_count = 1;
|
|
send_cfg->skip_compress_suffixes = calloc(1, sizeof(char*));
|
|
send_cfg->skip_compress_suffixes[0] = str_dup(".zip");
|
|
send_cfg->max_alloc = MAX_SERVER_ALLOC + 1;
|
|
|
|
/* Use socketpair for bidirectional communication */
|
|
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) {
|
|
/* Child: use p[0] for both read and write (connected to parent's p[1]) */
|
|
close(p[1]);
|
|
io_set_fds(p[0], p[0]);
|
|
Config* recv_cfg = config_receive(p[0]);
|
|
|
|
bool ok = true;
|
|
if (!recv_cfg)
|
|
ok = false;
|
|
else {
|
|
if (strcmp(recv_cfg->version, PROTOCOL_VERSION) != 0)
|
|
ok = false;
|
|
if (strcmp(recv_cfg->send_directory, "/send/src") != 0)
|
|
ok = false;
|
|
if (strcmp(recv_cfg->receive_root_directory, "/send/dst") != 0)
|
|
ok = false;
|
|
if (!recv_cfg->save_to_disk)
|
|
ok = false;
|
|
if (!recv_cfg->use_multithreading)
|
|
ok = false;
|
|
if (recv_cfg->use_chunk_serialization)
|
|
ok = false;
|
|
if (recv_cfg->compression_level != 5)
|
|
ok = false;
|
|
if (recv_cfg->chunk_size != 1024)
|
|
ok = false;
|
|
if (!recv_cfg->use_executability)
|
|
ok = false;
|
|
if (!recv_cfg->size_only)
|
|
ok = false;
|
|
if (!recv_cfg->ignore_times)
|
|
ok = false;
|
|
if (!recv_cfg->eight_bit_output)
|
|
ok = false;
|
|
if (recv_cfg->use_delta)
|
|
ok = false;
|
|
if (!recv_cfg->fuzzy)
|
|
ok = false;
|
|
if (recv_cfg->modify_window != 4)
|
|
ok = false;
|
|
if (!recv_cfg->existing)
|
|
ok = false;
|
|
if (!recv_cfg->ignore_existing)
|
|
ok = false;
|
|
if (!recv_cfg->delay_updates)
|
|
ok = false;
|
|
if (!recv_cfg->relative)
|
|
ok = false;
|
|
if (!recv_cfg->mkpath)
|
|
ok = false;
|
|
if (!recv_cfg->skip_compress_set || recv_cfg->skip_compress_count != 1 ||
|
|
strcmp(recv_cfg->skip_compress_suffixes[0], ".zip") != 0)
|
|
ok = false;
|
|
if (recv_cfg->max_alloc != MAX_SERVER_ALLOC)
|
|
ok = false;
|
|
}
|
|
config_delete(recv_cfg);
|
|
close(p[0]);
|
|
close(p[1]);
|
|
_exit(ok ? 0 : 1);
|
|
} else {
|
|
/* Parent: use p[1] for both read and write (connected to child's p[0]) */
|
|
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[0]);
|
|
close(p[1]);
|
|
|
|
config_delete(send_cfg);
|
|
|
|
EXPECT_TRUE(sent);
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
}
|
|
|
|
static void test_config_send_receive_version_mismatch() {
|
|
/* A peer using the previous wire format must be rejected. */
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
free(cfg->version);
|
|
cfg->version = str_dup("2.3.0");
|
|
cfg->send_directory = str_dup("/src");
|
|
cfg->receive_root_directory = str_dup("/dst");
|
|
|
|
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]);
|
|
close(p[0]);
|
|
_exit(recv == NULL ? 0 : 1);
|
|
} else {
|
|
close(p[0]);
|
|
io_set_fds(p[1], p[1]);
|
|
bool sent = config_send(p[1], cfg);
|
|
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
|
|
close(p[0]);
|
|
close(p[1]);
|
|
|
|
config_delete(cfg);
|
|
|
|
/* config_send receives STATUS_ERROR from config_receive, returns false */
|
|
EXPECT_FALSE(sent);
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
}
|
|
|
|
static void test_config_receive_truncated() {
|
|
int p[2];
|
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
|
io_set_fds(p[0], p[0]);
|
|
io_set_bwlimit(0);
|
|
|
|
/* A valid prefix exercises cleanup after allocated wire strings and a
|
|
* partially received scalar field. */
|
|
EXPECT_TRUE(send_str(p[1], PROTOCOL_VERSION));
|
|
unsigned long long max_alloc = DEFAULT_MAX_ALLOC;
|
|
EXPECT_TRUE(send_n_data(p[1], &max_alloc, sizeof(max_alloc)));
|
|
EXPECT_TRUE(send_str(p[1], "/src"));
|
|
EXPECT_TRUE(send_str(p[1], "/dst"));
|
|
EXPECT_TRUE(send_int(p[1], 1));
|
|
shutdown(p[1], SHUT_WR);
|
|
|
|
const Config* cfg = config_receive(p[0]);
|
|
EXPECT_NULL(cfg);
|
|
close(p[0]);
|
|
close(p[1]);
|
|
}
|
|
|
|
static bool config_string_roundtrip_matches(const Config* send_cfg, Config* recv) {
|
|
/* The sender serializes NULL strings as "" on the wire. Receivers must
|
|
canonicalize those empty values back to NULL for the options whose client
|
|
default is NULL (backup_dir, temp_dir, partial_dir, suffix), while a real
|
|
non-empty value round-trips unchanged. */
|
|
const char* fields[4];
|
|
char* const* recv_fields[4];
|
|
fields[0] = send_cfg->backup_dir;
|
|
recv_fields[0] = &recv->backup_dir;
|
|
fields[1] = send_cfg->temp_dir;
|
|
recv_fields[1] = &recv->temp_dir;
|
|
fields[2] = send_cfg->partial_dir;
|
|
recv_fields[2] = &recv->partial_dir;
|
|
fields[3] = send_cfg->suffix;
|
|
recv_fields[3] = &recv->suffix;
|
|
for (int i = 0; i < 4; i++) {
|
|
const char* sent = fields[i];
|
|
const char* got = *recv_fields[i];
|
|
if (sent == NULL || sent[0] == '\0') {
|
|
if (got != NULL)
|
|
return false;
|
|
} else if (got == NULL || strcmp(sent, got) != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool roundtrip_config_ok(const Config* send_cfg) {
|
|
int p[2];
|
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, p) != 0)
|
|
return false;
|
|
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;
|
|
if (ok) {
|
|
ok = recv->version != NULL && strcmp(recv->version, PROTOCOL_VERSION) == 0;
|
|
ok = ok && recv->send_directory && recv->receive_root_directory;
|
|
ok = ok && config_string_roundtrip_matches(send_cfg, recv);
|
|
}
|
|
config_delete(recv);
|
|
close(p[0]);
|
|
_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]);
|
|
return sent && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
}
|
|
}
|
|
|
|
/* Issue #252: NULL-vs-empty must survive the wire for backup_dir, temp_dir,
|
|
partial_dir, and suffix. NULL and explicitly-empty client values are both
|
|
serialized as "" and must be reconstructed as NULL so plain --backup (with
|
|
no --suffix/--backup-dir) works exactly like the client configured it. */
|
|
static void test_config_string_null_vs_empty_roundtrip() {
|
|
if (is_running_under_valgrind())
|
|
return;
|
|
|
|
/* NULL values on the wire must come back as NULL. */
|
|
Config* a = config_create();
|
|
EXPECT_NOT_NULL(a);
|
|
a->send_directory = str_dup("/src");
|
|
a->receive_root_directory = str_dup("/dst");
|
|
EXPECT_TRUE(roundtrip_config_ok(a));
|
|
config_delete(a);
|
|
|
|
/* Explicitly empty strings (indistinguishable on the wire from NULL) must
|
|
be canonicalized to NULL by the receiver. */
|
|
Config* b = config_create();
|
|
EXPECT_NOT_NULL(b);
|
|
b->send_directory = str_dup("/src");
|
|
b->receive_root_directory = str_dup("/dst");
|
|
b->backup_dir = str_dup("");
|
|
b->temp_dir = str_dup("");
|
|
b->partial_dir = str_dup("");
|
|
b->suffix = str_dup("");
|
|
EXPECT_TRUE(roundtrip_config_ok(b));
|
|
config_delete(b);
|
|
|
|
/* Non-empty values must round-trip unchanged. */
|
|
Config* c = config_create();
|
|
EXPECT_NOT_NULL(c);
|
|
c->send_directory = str_dup("/src");
|
|
c->receive_root_directory = str_dup("/dst");
|
|
c->backup_dir = str_dup("backups");
|
|
c->temp_dir = str_dup("/tmp/fast");
|
|
c->partial_dir = str_dup(".partial");
|
|
c->suffix = str_dup(".bak");
|
|
EXPECT_TRUE(roundtrip_config_ok(c));
|
|
config_delete(c);
|
|
}
|
|
|
|
/* A --temp-dir value must survive config_send/config_receive unchanged on the
|
|
receive side (round-trips through the resume-options wire block). */
|
|
static void test_config_temp_dir_roundtrip() {
|
|
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->temp_dir = str_dup("scratch");
|
|
EXPECT_TRUE(roundtrip_config_ok(c));
|
|
config_delete(c);
|
|
|
|
/* An empty-STRING wire value is canonicalized back to NULL (never an empty
|
|
scratch-dir name). */
|
|
c = config_create();
|
|
EXPECT_NOT_NULL(c);
|
|
c->send_directory = str_dup("/src");
|
|
c->receive_root_directory = str_dup("/dst");
|
|
c->temp_dir = str_dup("");
|
|
EXPECT_TRUE(roundtrip_config_ok(c));
|
|
config_delete(c);
|
|
}
|
|
|
|
static void test_config_delay_updates_reserved_backup_rejected() {
|
|
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->delay_updates = true;
|
|
c->backup_dir = str_dup(".fastsync-stage");
|
|
/* The receiver-side wire validation must reject a --backup-dir that collides
|
|
with the internal delay-updates staging directory. */
|
|
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->delay_updates = true;
|
|
c->backup_dir = str_dup("backups");
|
|
EXPECT_TRUE(roundtrip_config_ok(c));
|
|
config_delete(c);
|
|
}
|
|
|
|
static void test_config_delete_timing_early_helper() {
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
EXPECT_FALSE(config_delete_timing_early(cfg));
|
|
EXPECT_TRUE(config_has_valid_delete_timing(cfg));
|
|
cfg->use_delete = true;
|
|
EXPECT_TRUE(config_has_valid_delete_timing(cfg));
|
|
EXPECT_FALSE(config_delete_timing_early(cfg));
|
|
config_delete(cfg);
|
|
|
|
cfg = config_create();
|
|
cfg->use_delete = true;
|
|
cfg->delete_before = true;
|
|
EXPECT_TRUE(config_delete_timing_early(cfg));
|
|
EXPECT_TRUE(config_has_valid_delete_timing(cfg));
|
|
config_delete(cfg);
|
|
|
|
cfg = config_create();
|
|
cfg->use_delete = true;
|
|
cfg->delete_during = true;
|
|
EXPECT_TRUE(config_delete_timing_early(cfg));
|
|
EXPECT_TRUE(config_has_valid_delete_timing(cfg));
|
|
config_delete(cfg);
|
|
|
|
cfg = config_create();
|
|
cfg->use_delete = true;
|
|
cfg->delete_delay = true;
|
|
EXPECT_FALSE(config_delete_timing_early(cfg));
|
|
EXPECT_TRUE(config_has_valid_delete_timing(cfg));
|
|
config_delete(cfg);
|
|
|
|
cfg = config_create();
|
|
cfg->use_delete = true;
|
|
cfg->delete_after = true;
|
|
EXPECT_FALSE(config_delete_timing_early(cfg));
|
|
EXPECT_TRUE(config_has_valid_delete_timing(cfg));
|
|
config_delete(cfg);
|
|
|
|
/* Two simultaneous timings are invalid. */
|
|
cfg = config_create();
|
|
cfg->use_delete = true;
|
|
cfg->delete_before = true;
|
|
cfg->delete_after = true;
|
|
EXPECT_TRUE(config_delete_timing_early(cfg));
|
|
EXPECT_FALSE(config_has_valid_delete_timing(cfg));
|
|
config_delete(cfg);
|
|
|
|
/* A timing flag without deletion is invalid. */
|
|
cfg = config_create();
|
|
cfg->delete_delay = true;
|
|
EXPECT_FALSE(config_has_valid_delete_timing(cfg));
|
|
EXPECT_FALSE(config_delete_timing_early(cfg));
|
|
config_delete(cfg);
|
|
}
|
|
|
|
/* New delete-timing fields must survive config_send/config_receive unchanged,
|
|
and a config carrying two conflicting timings must be rejected. */
|
|
static void test_config_delete_timing_wire_roundtrip() {
|
|
if (is_running_under_valgrind())
|
|
return;
|
|
|
|
struct {
|
|
bool before, during, delay, after;
|
|
} cases[] = {
|
|
{false, false, false, false}, {true, false, false, false}, {false, true, false, false},
|
|
{false, false, true, false}, {false, false, false, true},
|
|
};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
int p[2];
|
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 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;
|
|
if (ok) {
|
|
ok = recv->use_delete && recv->delete_before == cases[i].before &&
|
|
recv->delete_during == cases[i].during && recv->delete_delay == cases[i].delay &&
|
|
recv->delete_after == cases[i].after;
|
|
}
|
|
config_delete(recv);
|
|
close(p[0]);
|
|
_exit(ok ? 0 : 1);
|
|
} else {
|
|
close(p[0]);
|
|
io_set_fds(p[1], p[1]);
|
|
Config* send_cfg = config_create();
|
|
EXPECT_NOT_NULL(send_cfg);
|
|
send_cfg->send_directory = str_dup("/src");
|
|
send_cfg->receive_root_directory = str_dup("/dst");
|
|
send_cfg->use_delete = true;
|
|
send_cfg->delete_before = cases[i].before;
|
|
send_cfg->delete_during = cases[i].during;
|
|
send_cfg->delete_delay = cases[i].delay;
|
|
send_cfg->delete_after = cases[i].after;
|
|
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-side wire validation rejects a keep-set config with two
|
|
conflicting delete-timing flags. */
|
|
static void test_config_delete_timing_conflict_rejected() {
|
|
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->use_delete = true;
|
|
c->delete_before = true;
|
|
c->delete_delay = true;
|
|
EXPECT_FALSE(roundtrip_config_ok(c));
|
|
config_delete(c);
|
|
}
|
|
|
|
/* The deletion-policy fields that cross the wire survive a config round trip:
|
|
--force (force_delete), --delete-excluded, --prune-empty-dirs and the
|
|
--max-delete number (default -1 == no client limit). */
|
|
static void test_config_delete_policy_wire_roundtrip() {
|
|
if (is_running_under_valgrind())
|
|
return;
|
|
|
|
struct {
|
|
bool force_delete, delete_excluded, prune_empty_dirs;
|
|
int max_delete;
|
|
} cases[] = {
|
|
{false, false, false, -1},
|
|
{true, false, false, 0},
|
|
{false, true, true, 7},
|
|
};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
int p[2];
|
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 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;
|
|
if (ok) {
|
|
ok = recv->force_delete == cases[i].force_delete &&
|
|
recv->delete_excluded == cases[i].delete_excluded &&
|
|
recv->prune_empty_dirs == cases[i].prune_empty_dirs &&
|
|
recv->max_delete == cases[i].max_delete;
|
|
}
|
|
config_delete(recv);
|
|
close(p[0]);
|
|
_exit(ok ? 0 : 1);
|
|
} else {
|
|
close(p[0]);
|
|
io_set_fds(p[1], p[1]);
|
|
Config* send_cfg = config_create();
|
|
EXPECT_NOT_NULL(send_cfg);
|
|
send_cfg->send_directory = str_dup("/src");
|
|
send_cfg->receive_root_directory = str_dup("/dst");
|
|
send_cfg->force_delete = cases[i].force_delete;
|
|
send_cfg->delete_excluded = cases[i].delete_excluded;
|
|
send_cfg->prune_empty_dirs = cases[i].prune_empty_dirs;
|
|
send_cfg->max_delete = cases[i].max_delete;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Basis-dir paths are canonicalized on the way in: trailing slashes and
|
|
interior empty / "." components are dropped so validation, the delete-walker
|
|
prefix and the receiver lookup all agree on one stored form. */
|
|
static void test_config_basis_normalization() {
|
|
Config* c = config_create();
|
|
EXPECT_NOT_NULL(c);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "prior/"), 0);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "a//b"), 0);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "./x/./y/"), 0);
|
|
EXPECT_EQ_INT(c->basis_count, 3);
|
|
EXPECT_EQ_STR(c->basis_dirs[0].path, "prior");
|
|
EXPECT_EQ_STR(c->basis_dirs[1].path, "a/b");
|
|
EXPECT_EQ_STR(c->basis_dirs[2].path, "x/y");
|
|
|
|
/* Degenerate values that normalize away to nothing stay rejected. */
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "."), -1);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, ".."), -1);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "/abs"), -1);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, "a/../b"), -1);
|
|
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_LINK, ""), -1);
|
|
config_delete(c);
|
|
}
|
|
|
|
static void test_config_is_remote_dest() {
|
|
/* Valid SSH-style destinations */
|
|
EXPECT_TRUE(config_is_remote_dest("user@host:/path"));
|
|
EXPECT_TRUE(config_is_remote_dest("host:/path"));
|
|
EXPECT_TRUE(config_is_remote_dest("user@192.168.1.1:/remote/path"));
|
|
|
|
/* Invalid destinations */
|
|
EXPECT_FALSE(config_is_remote_dest(NULL));
|
|
EXPECT_FALSE(config_is_remote_dest(""));
|
|
EXPECT_FALSE(config_is_remote_dest(":"));
|
|
EXPECT_FALSE(config_is_remote_dest("/local/path"));
|
|
EXPECT_FALSE(config_is_remote_dest("relative/path"));
|
|
/* C:/windows/path is treated as remote (colon with no preceding slash) */
|
|
EXPECT_TRUE(config_is_remote_dest("C:/windows/path"));
|
|
|
|
/* Edge cases */
|
|
EXPECT_FALSE(config_is_remote_dest("noslash"));
|
|
EXPECT_FALSE(config_is_remote_dest("/"));
|
|
EXPECT_TRUE(config_is_remote_dest("host:"));
|
|
EXPECT_TRUE(config_is_remote_dest("user@host:"));
|
|
}
|
|
|
|
/* The append-mode fields cross the wire unchanged: --append and --append-verify
|
|
are negotiated to the receiver so it knows to reply STATUS_APPEND on a
|
|
shorter destination. */
|
|
static void test_config_append_wire_roundtrip() {
|
|
if (is_running_under_valgrind())
|
|
return;
|
|
struct {
|
|
bool append, append_verify;
|
|
} cases[] = {{true, false}, {false, true}, {true, true}, {false, false}};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
int p[2];
|
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 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;
|
|
if (ok)
|
|
ok = recv->append == cases[i].append && recv->append_verify == cases[i].append_verify;
|
|
config_delete(recv);
|
|
close(p[0]);
|
|
_exit(ok ? 0 : 1);
|
|
} else {
|
|
close(p[0]);
|
|
io_set_fds(p[1], p[1]);
|
|
Config* send_cfg = config_create();
|
|
EXPECT_NOT_NULL(send_cfg);
|
|
send_cfg->send_directory = str_dup("/src");
|
|
send_cfg->receive_root_directory = str_dup("/dst");
|
|
send_cfg->append = cases[i].append;
|
|
send_cfg->append_verify = cases[i].append_verify;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void test_config() {
|
|
test_config_lifecycle();
|
|
test_config_ssh_dest();
|
|
test_config_ssh_dest_local_path();
|
|
test_config_ssh_dest_no_user();
|
|
test_pipeline_sender_lifecycle();
|
|
test_pipeline_receiver_lifecycle();
|
|
if (!is_running_under_valgrind()) {
|
|
test_config_send_receive();
|
|
test_config_send_receive_version_mismatch();
|
|
test_config_receive_truncated();
|
|
test_config_string_null_vs_empty_roundtrip();
|
|
test_config_temp_dir_roundtrip();
|
|
test_config_delay_updates_reserved_backup_rejected();
|
|
test_config_delete_timing_wire_roundtrip();
|
|
test_config_delete_timing_conflict_rejected();
|
|
test_config_delete_policy_wire_roundtrip();
|
|
test_config_append_wire_roundtrip();
|
|
test_config_basis_roundtrip();
|
|
test_config_basis_wire_rejects_escaping();
|
|
test_config_basis_normalization();
|
|
}
|
|
test_config_delete_timing_early_helper();
|
|
test_config_is_remote_dest();
|
|
}
|