Merge remote-tracking branch 'origin/feat/rsync-no-options' into dev

This commit is contained in:
2026-09-05 02:30:50 +02:00
7 changed files with 160 additions and 12 deletions
+1 -1
View File
@@ -235,7 +235,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 | |
| `--secluded-args` | Use protocol to send args | 🔄 Compatibility No-op | Accepted for CLI compatibility; it does not change FastSync transport or protocol behavior. `-s` remains chunk serialization. | | `--secluded-args` | Use protocol to send args | 🔄 Compatibility No-op | Accepted for CLI compatibility; it does not change FastSync transport or protocol behavior. `-s` remains chunk serialization. |
| `--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. |
--- ---
+73 -9
View File
@@ -345,6 +345,13 @@ typedef struct {
} OptionEntry; } OptionEntry;
/* Options parsed directly into Config, plus compatibility options with no effect. */ /* Options parsed directly into Config, plus compatibility options with no effect. */
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. */
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)},
{"--remove-source-files", NULL, OPT_FLAG, offsetof(Config, remove_source_files)}, {"--remove-source-files", NULL, OPT_FLAG, offsetof(Config, remove_source_files)},
@@ -398,6 +405,35 @@ 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)},
{"compress", "z", 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);
} }
@@ -429,6 +465,31 @@ static const OptionEntry* find_table_option_with_equals(const char* arg, const c
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;
if (entry->offset == offsetof(Config, use_metadata))
config->metadata_explicitly_disabled = true;
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) {
if (entry->kind == OPT_NOOP) if (entry->kind == OPT_NOOP)
return 0; return 0;
@@ -479,6 +540,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
} }
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 char* modify_window_prefix = "--modify-window="; const char* modify_window_prefix = "--modify-window=";
if (strncmp(argv[i], modify_window_prefix, strlen(modify_window_prefix)) == 0) { if (strncmp(argv[i], modify_window_prefix, strlen(modify_window_prefix)) == 0) {
if (set_nonneg_int_option(&config->modify_window, argv[i] + strlen(modify_window_prefix), if (set_nonneg_int_option(&config->modify_window, argv[i] + strlen(modify_window_prefix),
@@ -762,6 +828,13 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING)); set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING));
if (config->compress_choice) if (config->compress_choice)
config->use_compression = strcmp(config->compress_choice, "zstd") == 0; config->use_compression = strcmp(config->compress_choice, "zstd") == 0;
/* Incremental and delta transfers need metadata unless the user disabled it. */
if ((config->use_incremental || config->use_delta) && !config->use_metadata &&
!config->metadata_explicitly_disabled) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for incremental/delta transfer");
config->use_metadata = true;
}
return 0; return 0;
} }
@@ -871,15 +944,6 @@ int main(int argc, char* argv[]) {
goto cleanup; goto cleanup;
} }
/* Enable implicit flags */
if (config->use_incremental && !config->use_metadata) {
log_info_message(LOG_INFO_MISC, "Enabling metadata preservation for --incremental");
config->use_metadata = true;
}
if (config->use_delta && !config->use_metadata) {
log_info_message(LOG_INFO_MISC, "Enabling metadata preservation for --delta");
config->use_metadata = true;
}
/* Initialize TLS if needed */ /* Initialize TLS if needed */
if (config->use_tls) if (config->use_tls)
tls_global_init(); tls_global_init();
+1
View File
@@ -94,6 +94,7 @@ void print_usage(void) {
printf(" --zl <n> Alias for --compress-level\n"); printf(" --zl <n> Alias for --compress-level\n");
printf(" --skip-compress=LIST Skip compression for comma-separated suffixes\n"); printf(" --skip-compress=LIST Skip compression for comma-separated suffixes\n");
printf(" --compress-threads <n> Compression worker threads (requires zstd threaded support)\n"); printf(" --compress-threads <n> Compression worker threads (requires zstd threaded support)\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");
} }
+1
View File
@@ -19,6 +19,7 @@ static void config_set_defaults(Config* config) {
config->use_compression = false; config->use_compression = false;
config->use_metadata = false; config->use_metadata = false;
config->use_executability = false; config->use_executability = false;
config->metadata_explicitly_disabled = false;
config->show_progress = false; config->show_progress = false;
config->dry_run = false; config->dry_run = false;
config->remove_source_files = false; config->remove_source_files = false;
+1
View File
@@ -19,6 +19,7 @@ typedef struct Config {
bool use_sendfile; bool use_sendfile;
bool use_metadata; bool use_metadata;
bool use_executability; bool use_executability;
bool metadata_explicitly_disabled;
bool show_progress; bool show_progress;
bool dry_run; bool dry_run;
bool remove_source_files; bool remove_source_files;
+14
View File
@@ -169,6 +169,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 TestExecutability: class TestExecutability:
def test_preserves_only_executable_bits(self, shared_server): def test_preserves_only_executable_bits(self, shared_server):
+67
View File
@@ -703,6 +703,69 @@ 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-z", "-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_no_preserve_blocks_implicit_metadata() {
static const char* const options[][3] = {
{"--incremental", "--no-preserve", "/src"},
{"--no-preserve", "--incremental", "/src"},
{"--delta", "--no-preserve", "/src"},
{"--no-preserve", "--delta", "/src"},
};
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
Config* cfg = config_create();
char* argv[] = {"fastsync", (char*)options[i][0], (char*)options[i][1], (char*)options[i][2],
"/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
EXPECT_FALSE(cfg->use_metadata);
EXPECT_TRUE(cfg->metadata_explicitly_disabled);
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);
}
}
static void test_parse_args_fsync() { static void test_parse_args_fsync() {
Config* cfg = config_create(); Config* cfg = config_create();
char* argv[] = {"fastsync", "--fsync", "/src", "/dst"}; char* argv[] = {"fastsync", "--fsync", "/src", "/dst"};
@@ -969,6 +1032,10 @@ void test_client_cli() {
test_parse_args_info_verbose_order(); test_parse_args_info_verbose_order();
test_parse_args_rejects_invalid_info_flag(); test_parse_args_rejects_invalid_info_flag();
test_parse_args_archive(); test_parse_args_archive();
test_parse_args_negations();
test_parse_args_negation_order();
test_parse_args_no_preserve_blocks_implicit_metadata();
test_parse_args_rejects_unsafe_negation();
test_parse_args_fsync(); test_parse_args_fsync();
test_parse_args_existing(); test_parse_args_existing();
test_parse_args_ignore_times(); test_parse_args_ignore_times();