Merge feat/p6-protocol: --protocol version-force flag (client-only)

This commit is contained in:
2026-09-10 20:44:44 +02:00
5 changed files with 143 additions and 2 deletions
+6
View File
@@ -598,6 +598,12 @@ static const OptionEntry OPTION_TABLE[] = {
* charsets at startup (client_validation.c) and the full spec rides the
* config frame so the receiver derives the wire charset symmetrically. */
{"--iconv", NULL, OPT_STRING, offsetof(Config, iconv_spec)},
/* --protocol=NUM: rsync-compatible flag that forces the wire protocol
* version to the current value. FastSync has exactly one wire format, so
* any value other than PROTOCOL_VERSION is rejected at validation, before
* any network I/O. Client-only: the server does not negotiate, it just
* enforces an exact match. */
{"--protocol", NULL, OPT_STRING, offsetof(Config, version)},
{"--delete-before", NULL, OPT_FLAG, offsetof(Config, delete_before)},
{"--delete-during", "--del", OPT_FLAG, offsetof(Config, delete_during)},
{"--delete-delay", NULL, OPT_FLAG, offsetof(Config, delete_delay)},
+11
View File
@@ -3,6 +3,7 @@
#include "delay_updates.h"
#include "log.h"
#include "usage.h"
#include <string.h>
#include <stdio.h>
/* Validate config after parsing. Returns true if valid. */
@@ -132,5 +133,15 @@ bool validate_config(const Config* config) {
"--iconv requires LOCAL[,REMOTE] charset names supported by iconv");
return false;
}
/* --protocol: FastSync has exactly one wire format, so the forced version
must equal the current PROTOCOL_VERSION exactly. Rejected here, before any
network I/O, rather than letting the server hit its own mismatch check. */
if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
log_message(LOG_LEVEL_ERROR,
"--protocol must be %s (FastSync supports only its current wire "
"protocol version and cannot speak an older or virtual one)",
PROTOCOL_VERSION);
return false;
}
return true;
}
+3
View File
@@ -41,6 +41,9 @@ void print_usage(void) {
printf(" converted before transmission and back on receipt; a\n");
printf(" name that cannot be represented in the target charset\n");
printf(" fails that transfer cleanly (rsync-compatible)\n");
printf(" --protocol=NUM Force the wire protocol version (must equal the current\n");
printf(" PROTOCOL_VERSION; FastSync cannot speak older/virtual\n");
printf(" wire formats)\n");
printf(" --delete Delete files on receiver not in source\n");
printf(" (default timing: delete only after the whole\n");
printf(" transfer has succeeded)\n");
+57 -1
View File
@@ -2,10 +2,11 @@
import subprocess
import sys
import os
import shutil
import pytest
sys.path.insert(0, os.path.dirname(__file__))
from common import BUILD_DIR, CLIENT_CMD, SERVER_CMD
from common import BUILD_DIR, CLIENT_CMD, SERVER_CMD, TEST_DATA_DIR, run_client, verify_transfer
class TestHelp:
@@ -79,3 +80,58 @@ class TestServerPort:
finally:
proc.terminate()
proc.wait(timeout=5)
def _seed_protocol_source(source):
os.makedirs(source, exist_ok=True)
with open(os.path.join(source, "p.txt"), "w") as fh:
fh.write("protocol test\n")
os.makedirs(os.path.join(source, "nested"), exist_ok=True)
with open(os.path.join(source, "nested", "deep.txt"), "w") as fh:
fh.write("deep file\n")
class TestProtocol:
@pytest.mark.ci
def test_protocol_current_version_accepted(self, shared_server):
"""--protocol=2.16.0 (the current PROTOCOL_VERSION) is accepted and the
transfer completes normally."""
source = os.path.join(TEST_DATA_DIR, "proto_ok_src")
dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
result, _ = run_client(source, dest, flags=["--protocol=2.16.0"],
port=shared_server.port)
assert result.returncode == 0, \
f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}"
received = os.path.join(dest, os.path.abspath(source).lstrip(os.sep))
mismatches, missing = verify_transfer(source, received)
assert not mismatches and not missing, \
f"transfer mismatch: missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_protocol_rejects_other_versions(self, shared_server):
"""Other versions are rejected up front, before connecting."""
source = os.path.join(TEST_DATA_DIR, "proto_reject_src")
dest = os.path.join(TEST_DATA_DIR, "proto_reject_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
for bad in ("2.15.0", "2.17.0", "216", "31"):
result, _ = run_client(source, dest, flags=[f"--protocol={bad}"],
port=shared_server.port)
assert result.returncode != 0, f"--protocol={bad} should be rejected"
@pytest.mark.ci
def test_protocol_rejects_garbage(self, shared_server):
"""Garbage/empty --protocol values are rejected up front."""
source = os.path.join(TEST_DATA_DIR, "proto_garbage_src")
dest = os.path.join(TEST_DATA_DIR, "proto_garbage_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
for bad in ("abc", ""):
result, _ = run_client(source, dest, flags=[f"--protocol={bad}"],
port=shared_server.port)
assert result.returncode != 0, f"--protocol={bad} should be rejected"
+66 -1
View File
@@ -210,6 +210,67 @@ static void test_parse_args_version() {
config_delete(cfg);
}
/* --protocol=NUM forces the wire protocol version: the current PROTOCOL_VERSION
* is accepted (stored into config->version, which the config frame transmits),
* and any other value is rejected. Client-only: no server-side flag exists. */
static void test_parse_args_protocol_accept_current() {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char* argv_equals[] = {"fastsync", "--source-dir", "/src",
"--dest-dir", "/dst", "--protocol=2.16.0"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 6, argv_equals, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION);
config_delete(cfg);
cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir",
"/dst", "--protocol", "2.16.0"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION);
config_delete(cfg);
}
/* Any --protocol value other than the current PROTOCOL_VERSION must end in
* failure (parse_args simply stores it; validate_config rejects it up front). */
static void test_parse_args_protocol_rejects_other_versions() {
static const char* const bad_versions[] = {"2.16", "2.15.0", "2.17.0", "216", "31", "abc", ""};
for (size_t i = 0; i < sizeof(bad_versions) / sizeof(bad_versions[0]); i++) {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char arg[64];
snprintf(arg, sizeof(arg), "--protocol=%s", bad_versions[i]);
char* argv[] = {"fastsync", "--source-dir", "/src", "--dest-dir", "/dst", arg};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 6, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(strcmp(cfg->version, PROTOCOL_VERSION) != 0);
EXPECT_FALSE(validate_config(cfg));
config_delete(cfg);
}
}
/* validate_config accepts the current PROTOCOL_VERSION (the default) and rejects
* a version that does not equal it -- the honest post-parse enforcement. */
static void test_validate_config_protocol_version() {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION);
EXPECT_TRUE(validate_config(cfg));
config_delete(cfg);
cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("2.15.0");
EXPECT_NOT_NULL(cfg->version);
EXPECT_FALSE(validate_config(cfg));
config_delete(cfg);
}
/* --xattrs/-X and --acls/-A preserve per-file xattrs and both imply metadata
* transmission (the xattr block rides the metadata/per-file frame); each is
* individually negatable and the derived use_xattrs follows the flags. */
@@ -1794,7 +1855,8 @@ static void test_parse_args_table_equals_string_and_int_options() {
* generic "Unknown option", when they are the final argv entry. */
static void test_parse_args_missing_argument_diagnostic() {
static const char* const options[] = {"--exclude", "--server-port", "--skip-compress",
"-T", "--out-format", "--log-file-format"};
"-T", "--out-format", "--log-file-format",
"--protocol"};
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
Config* cfg = config_create();
@@ -2850,6 +2912,9 @@ void test_client_cli() {
test_cli_exclude_patterns();
test_parse_args_help();
test_parse_args_version();
test_parse_args_protocol_accept_current();
test_parse_args_protocol_rejects_other_versions();
test_validate_config_protocol_version();
test_parse_args_valid_port();
test_parse_args_size_only();
test_parse_args_ignore_existing();