refactor(config,metadata): shared invariants; length-bounded metadata parser

This commit is contained in:
2026-09-13 05:24:23 +02:00
parent 57ce6d04f0
commit eea66a7848
11 changed files with 422 additions and 280 deletions
+152
View File
@@ -2042,6 +2042,156 @@ static void test_super_does_not_imply_numeric() {
config_delete(c);
}
/* The single shared predicate must reject every cross-field combination the
client/server enforce and accept a plain valid config. Because both
validate_config() (client) and validate_received_config() (server) call it,
this table documents the whole invariant set in one place. */
static void test_config_invariants_error_all_combinations() {
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
EXPECT_NULL(config_invariants_error(c));
config_delete(c);
c = config_create();
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_COMPARE, "sub"), 0);
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* basis + chunk */
config_delete(c);
c = config_create();
c->use_sendfile = true;
c->use_compression = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* sendfile + compression */
config_delete(c);
c = config_create();
c->use_sendfile = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* sendfile + chunk */
config_delete(c);
c = config_create();
c->use_incremental = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* incremental + chunk */
config_delete(c);
c = config_create();
c->skip_compress_set = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* skip-compress + chunk */
config_delete(c);
c = config_create();
c->use_delta = true; /* whole_file false -> active */
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta without incremental */
config_delete(c);
c = config_create();
c->use_delta = true;
c->use_incremental = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta + chunk */
config_delete(c);
c = config_create();
c->use_delta = true;
c->use_incremental = true;
c->use_sendfile = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta + sendfile */
config_delete(c);
c = config_create();
c->append = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* append + chunk */
config_delete(c);
c = config_create();
c->append = true;
c->whole_file = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* append + whole-file */
config_delete(c);
c = config_create();
c->preserve_hard_links = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* hard-links + chunk */
config_delete(c);
c = config_create();
c->preserve_xattrs = true;
c->use_chunk_serialization = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* xattrs + chunk */
config_delete(c);
c = config_create();
c->preserve_hard_links = true;
c->append = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* hard-links + append */
config_delete(c);
c = config_create();
c->delay_updates = true;
c->inplace = true;
EXPECT_NOT_NULL(config_invariants_error(c)); /* delay-updates + inplace */
config_delete(c);
c = config_create();
c->delay_updates = true;
c->backup_dir = str_dup(".fastsync-stage");
EXPECT_NOT_NULL(config_invariants_error(c)); /* delay-updates staging conflict */
config_delete(c);
c = config_create();
c->delete_delay = true; /* a timing flag without --delete */
EXPECT_NOT_NULL(config_invariants_error(c)); /* invalid delete timing */
config_delete(c);
c = config_create();
c->iconv_spec = str_dup("no-such-charset,utf-8");
EXPECT_NOT_NULL(config_invariants_error(c)); /* malformed iconv spec */
config_delete(c);
c = config_create();
c->copy_as_set = true;
c->use_metadata = false;
EXPECT_NOT_NULL(config_invariants_error(c)); /* copy-as without metadata */
config_delete(c);
}
/* The receiver previously missed several of these; a forged frame that sets
the offending serialized fields must now be refused at the config
handshake. (whole_file is client-only, so its rules cannot appear here.) */
static void test_config_receive_rejects_unified_invariants() {
if (is_running_under_valgrind())
return;
struct {
bool incremental, delta, chunk, sendfile, compression;
} cases[] = {
{true, false, true, false, false}, /* --incremental + -s */
{false, true, true, false, false}, /* --delta + -s */
{false, true, false, false, false}, /* --delta without --incremental */
{false, false, false, true, true}, /* --sendfile + compression */
{false, false, true, true, false}, /* --sendfile + -s */
};
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->use_incremental = cases[i].incremental;
c->use_delta = cases[i].delta;
c->use_chunk_serialization = cases[i].chunk;
c->use_sendfile = cases[i].sendfile;
c->use_compression = cases[i].compression;
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
}
}
void test_config() {
test_config_lifecycle();
test_config_ssh_dest();
@@ -2095,6 +2245,8 @@ void test_config() {
test_config_receive_rejects_copy_as_without_metadata();
test_config_receive_rejects_oversized_string_budget();
test_config_receive_with_validate_rejects();
test_config_invariants_error_all_combinations();
test_config_receive_rejects_unified_invariants();
}
test_identity_copy_as_refused();
test_identity_ownership_requested();