feat: support rsync no-option handling
CI / lint (pull_request) Successful in 12s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
CI / lint (pull_request) Successful in 12s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
This commit is contained in:
+1
-1
@@ -234,7 +234,7 @@ This document maps rsync's full feature set to FastSync's current implementation
|
|||||||
| `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | |
|
| `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | |
|
||||||
| `--checksum-seed=NUM` | Set checksum seed | ❌ Not Implemented | |
|
| `--checksum-seed=NUM` | Set checksum seed | ❌ Not Implemented | |
|
||||||
| `-s`, `--secluded-args` | Use protocol to send args | ❌ Not Implemented | |
|
| `-s`, `--secluded-args` | Use protocol to send args | ❌ Not Implemented | |
|
||||||
| `--no-OPTION` | Turn off implied option | ❌ Not Implemented | |
|
| `--no-OPTION` | Turn off implied option | ✅ Supported | Supported boolean FastSync options and archive-implied options; unsafe or value-taking options are rejected. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,12 @@ typedef struct {
|
|||||||
size_t offset; /* offsetof of the target field in Config */
|
size_t offset; /* offsetof of the target field in Config */
|
||||||
} OptionEntry;
|
} OptionEntry;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char* name;
|
||||||
|
const char* alias;
|
||||||
|
size_t offset; /* offsetof of the boolean target field in Config */
|
||||||
|
} NegatableOption;
|
||||||
|
|
||||||
/* Options that map directly onto a Config field with no side effects. */
|
/* Options that map directly onto a Config field with no side effects. */
|
||||||
static const OptionEntry OPTION_TABLE[] = {
|
static const OptionEntry OPTION_TABLE[] = {
|
||||||
{"--dry-run", "-n", OPT_FLAG, offsetof(Config, dry_run)},
|
{"--dry-run", "-n", OPT_FLAG, offsetof(Config, dry_run)},
|
||||||
@@ -173,6 +179,34 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
{"--min-size", NULL, OPT_ULL, offsetof(Config, min_size)},
|
{"--min-size", NULL, OPT_ULL, offsetof(Config, min_size)},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Only boolean options with no required argument are safe to negate. */
|
||||||
|
static const NegatableOption NEGATABLE_OPTIONS[] = {
|
||||||
|
{"dry-run", "n", offsetof(Config, dry_run)},
|
||||||
|
{"delete", NULL, offsetof(Config, use_delete)},
|
||||||
|
{"incremental", NULL, offsetof(Config, use_incremental)},
|
||||||
|
{"delta", NULL, offsetof(Config, use_delta)},
|
||||||
|
{"save-to-disk", NULL, offsetof(Config, save_to_disk)},
|
||||||
|
{"progress", NULL, offsetof(Config, show_progress)},
|
||||||
|
{"tls", NULL, offsetof(Config, use_tls)},
|
||||||
|
{"backup", NULL, offsetof(Config, backup)},
|
||||||
|
{"stats", NULL, offsetof(Config, stats)},
|
||||||
|
{"partial", NULL, offsetof(Config, partial)},
|
||||||
|
{"links", "l", offsetof(Config, follow_symlinks)},
|
||||||
|
{"copy-links", NULL, offsetof(Config, copy_links)},
|
||||||
|
{"safe-links", NULL, offsetof(Config, safe_links)},
|
||||||
|
{"copy-unsafe-links", NULL, offsetof(Config, copy_unsafe_links)},
|
||||||
|
{"sparse", "S", offsetof(Config, preserve_sparse)},
|
||||||
|
{"inplace", NULL, offsetof(Config, inplace)},
|
||||||
|
{"checksum", NULL, offsetof(Config, checksum)},
|
||||||
|
|
||||||
|
/* These options are also implied by --archive or handled outside the table. */
|
||||||
|
{"compress", "c", offsetof(Config, use_compression)},
|
||||||
|
{"multithreading", "m", offsetof(Config, use_multithreading)},
|
||||||
|
{"preserve", "M", offsetof(Config, use_metadata)},
|
||||||
|
{"sendfile", "f", offsetof(Config, use_sendfile)},
|
||||||
|
{"chunk-serialization", "s", offsetof(Config, use_chunk_serialization)},
|
||||||
|
};
|
||||||
|
|
||||||
static bool opt_is(const char* arg, const char* name, const char* alias) {
|
static bool opt_is(const char* arg, const char* name, const char* alias) {
|
||||||
return strcmp(arg, name) == 0 || (alias && strcmp(arg, alias) == 0);
|
return strcmp(arg, name) == 0 || (alias && strcmp(arg, alias) == 0);
|
||||||
}
|
}
|
||||||
@@ -184,6 +218,29 @@ static const OptionEntry* find_table_option(const char* arg) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const NegatableOption* find_negatable_option(const char* name) {
|
||||||
|
for (size_t i = 0; i < sizeof(NEGATABLE_OPTIONS) / sizeof(NEGATABLE_OPTIONS[0]); i++)
|
||||||
|
if (strcmp(name, NEGATABLE_OPTIONS[i].name) == 0 ||
|
||||||
|
(NEGATABLE_OPTIONS[i].alias && strcmp(name, NEGATABLE_OPTIONS[i].alias) == 0))
|
||||||
|
return &NEGATABLE_OPTIONS[i];
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int apply_negation(Config* config, const char* arg) {
|
||||||
|
const char* name = arg + strlen("--no-");
|
||||||
|
if (*name == '\0') {
|
||||||
|
fprintf(stderr, "Cannot negate an empty option name: %s\n", arg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const NegatableOption* entry = find_negatable_option(name);
|
||||||
|
if (!entry) {
|
||||||
|
fprintf(stderr, "Cannot negate unsupported or unsafe option: %s\n", arg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*(bool*)((char*)config + entry->offset) = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int apply_table_option(Config* config, const OptionEntry* entry, const char* value) {
|
static int apply_table_option(Config* config, const OptionEntry* entry, const char* value) {
|
||||||
void* field = (char*)config + entry->offset;
|
void* field = (char*)config + entry->offset;
|
||||||
switch (entry->kind) {
|
switch (entry->kind) {
|
||||||
@@ -211,6 +268,11 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch
|
|||||||
int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||||
int* positional_count) {
|
int* positional_count) {
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (strncmp(argv[i], "--no-", strlen("--no-")) == 0) {
|
||||||
|
if (apply_negation(config, argv[i]) != 0)
|
||||||
|
return -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const OptionEntry* entry = find_table_option(argv[i]);
|
const OptionEntry* entry = find_table_option(argv[i]);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
if (entry->kind != OPT_FLAG) {
|
if (entry->kind != OPT_FLAG) {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ void print_usage(void) {
|
|||||||
printf(" -S, --sparse Handle sparse files efficiently\n");
|
printf(" -S, --sparse Handle sparse files efficiently\n");
|
||||||
printf(" --inplace Update files in-place (no temp+rename)\n");
|
printf(" --inplace Update files in-place (no temp+rename)\n");
|
||||||
printf(" --compress-level <n> Compression level (default: 5)\n");
|
printf(" --compress-level <n> Compression level (default: 5)\n");
|
||||||
|
printf(" --no-OPTION Disable a supported boolean option\n");
|
||||||
printf(" --help Show this help\n");
|
printf(" --help Show this help\n");
|
||||||
printf(" -V, --version Show version\n");
|
printf(" -V, --version Show version\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,20 @@ class TestArchiveMode:
|
|||||||
assert not missing, f"Missing: {missing}"
|
assert not missing, f"Missing: {missing}"
|
||||||
assert not mismatches, f"Mismatch: {mismatches}"
|
assert not mismatches, f"Mismatch: {mismatches}"
|
||||||
|
|
||||||
|
def test_archive_implied_options_can_be_negated(self, shared_server):
|
||||||
|
clean_dir(DEST_DIR)
|
||||||
|
result, dur = run_client(
|
||||||
|
SOURCE_DIR, DEST_DIR,
|
||||||
|
flags=["--archive", "--no-compress", "--no-m", "--no-preserve"],
|
||||||
|
port=shared_server.port,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||||
|
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||||
|
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||||
|
assert not missing, f"Missing: {missing}"
|
||||||
|
assert not mismatches, f"Mismatch: {mismatches}"
|
||||||
|
|
||||||
|
|
||||||
class TestExclude:
|
class TestExclude:
|
||||||
def test_exclude_single(self, shared_server):
|
def test_exclude_single(self, shared_server):
|
||||||
|
|||||||
@@ -339,6 +339,47 @@ static void test_parse_args_archive() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Negations must override archive's implied options in argument order. */
|
||||||
|
static void test_parse_args_negations() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "--archive", "--no-compress", "--no-m",
|
||||||
|
"--no-preserve", "--no-dry-run", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 8, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_FALSE(cfg->use_compression);
|
||||||
|
EXPECT_FALSE(cfg->use_multithreading);
|
||||||
|
EXPECT_FALSE(cfg->use_metadata);
|
||||||
|
EXPECT_FALSE(cfg->dry_run);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_args_negation_order() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "--no-compress", "-c", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->use_compression);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_args_rejects_unsafe_negation() {
|
||||||
|
static const char* const options[] = {"--no-archive", "--no-timeout", "--no-unknown"};
|
||||||
|
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", (char*)options[i], "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void test_client_cli() {
|
void test_client_cli() {
|
||||||
test_validate_config_required_paths();
|
test_validate_config_required_paths();
|
||||||
test_validate_config_incompatible_options();
|
test_validate_config_incompatible_options();
|
||||||
@@ -360,4 +401,7 @@ void test_client_cli() {
|
|||||||
test_parse_args_unknown_option();
|
test_parse_args_unknown_option();
|
||||||
test_parse_args_rejects_unimplemented_options();
|
test_parse_args_rejects_unimplemented_options();
|
||||||
test_parse_args_archive();
|
test_parse_args_archive();
|
||||||
|
test_parse_args_negations();
|
||||||
|
test_parse_args_negation_order();
|
||||||
|
test_parse_args_rejects_unsafe_negation();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user