From cd27886dde4bd17950a494f9fffc47df42118b35 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 16:38:12 +0200 Subject: [PATCH 1/3] feat: add rsync compression aliases --- README.md | 2 ++ RSYNC_COMPAT.md | 4 ++-- src/client/client_cli.c | 3 ++- src/client/usage.c | 3 +++ tests/test_client_cli.c | 19 ++++++++++++++++++- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fdbd9f3..6873f7c 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,8 @@ features without changing the meaning of ordinary compatibility options. | `-m` | Enable the multithreaded scanner/loader/sender pipeline. | | `-c [level]`, `-z [level]` | Enable streaming zstd compression, levels 1-22. | | `--compress-level ` | Set the zstd compression level. | +| `--zc ` | Alias for `--compress-choice`. FastSync supports `zstd` and `none`. | +| `--zl ` | Alias for `--compress-level`. | | `--chunk-size ` | Set the transfer chunk size. | | `-s` | Enable FastSync chunk serialization. | | `-f`, `--sendfile` | Use TCP `sendfile()` zero-copy transfer. Incompatible with compression and chunk serialization. | diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..95a9224 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -171,8 +171,8 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `-z`, `--compress` | Compress file data | 🔀 Alt Arg | Always uses zstd (rsync supports multiple algorithms) | -| `--compress-choice=STR` | Choose compression algorithm | ❌ Not Implemented | Removed because it had no effect; FastSync always uses zstd | -| `--compress-level=NUM` | Set compression level | ✅ Implemented | 1-22, default 5 | +| `--compress-choice=STR`, `--zc=STR` | Choose compression algorithm | ✅ Implemented | FastSync supports `zstd` and `none` | +| `--compress-level=NUM`, `--zl=NUM` | Set compression level | ✅ Implemented | 1-22, default 5 | | `--compress-threads=NUM` | Set compression threads | ❌ Not Implemented | | | `--skip-compress=LIST` | Skip compress for suffixes | ❌ Not Implemented | Internal skip for hardcoded types; not user-configurable | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..dfe39ee 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -164,6 +164,7 @@ static const OptionEntry OPTION_TABLE[] = { {"--fastsync-server-path", NULL, OPT_STRING, offsetof(Config, fastsync_server_path)}, {"--partial-dir", NULL, OPT_STRING, offsetof(Config, partial_dir)}, {"--suffix", NULL, OPT_STRING, offsetof(Config, suffix)}, + {"--compress-choice", "--zc", OPT_STRING, offsetof(Config, compress_choice)}, {"--timeout", NULL, OPT_POS_INT, offsetof(Config, timeout)}, {"--contimeout", NULL, OPT_POS_INT, offsetof(Config, contimeout)}, @@ -354,7 +355,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (opt_is(argv[i], "-T", NULL) && i + 1 < argc) { if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; - } else if (opt_is(argv[i], "--compress-level", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--compress-level", "--zl") && i + 1 < argc) { if (set_positive_int_option(&config->compression_level, argv[++i], "--compress-level") != 0) return -1; if (config->compression_level < 1 || config->compression_level > 22) { diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..508136c 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -36,6 +36,8 @@ void print_usage(void) { printf(" -m Enable multithreading\n"); printf(" -s Enable chunk serialization\n"); printf(" -f Enable sendfile (TCP only, not with -c or -s)\n"); + printf(" --compress-choice Compression algorithm (default: zstd)\n"); + printf(" --zc Alias for --compress-choice\n"); printf(" -v, --verbose Enable debug logging\n"); printf(" -M, --preserve Preserve file metadata\n"); printf(" --chunk-size Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE); @@ -69,6 +71,7 @@ void print_usage(void) { printf(" -S, --sparse Handle sparse files efficiently\n"); printf(" --inplace Update files in-place (no temp+rename)\n"); printf(" --compress-level Compression level (default: 5)\n"); + printf(" --zl Alias for --compress-level\n"); printf(" --help Show this help\n"); printf(" -V, --version Show version\n"); } diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..9a9d7d1 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -310,7 +310,7 @@ static void test_parse_args_rejects_unimplemented_options() { "--daemon", "--config", "--server", - "--compress-choice"}; + "--checksum-choice"}; for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { Config* cfg = config_create(); @@ -339,6 +339,22 @@ static void test_parse_args_archive() { config_delete(cfg); } +/* Test rsync-compatible compression-choice and compression-level aliases. */ +static void test_parse_args_compression_aliases() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--zc", "zstd", "--zl", "10", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 7, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 0); + EXPECT_EQ_STR(cfg->compress_choice, "zstd"); + EXPECT_EQ_INT(cfg->compression_level, 10); + EXPECT_EQ_INT(positional_count, 2); + + config_delete(cfg); +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -360,4 +376,5 @@ void test_client_cli() { test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); + test_parse_args_compression_aliases(); } From 4152bf379ef78d9ecf3e10bf124fc1d656696435 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 21:44:21 +0200 Subject: [PATCH 2/3] fix: honor compression choice aliases --- src/client/client_cli.c | 70 +++++++++++++++++++++++++----- tests/integration/test_features.py | 22 ++++++++++ tests/test_client_cli.c | 27 ++++++++++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/src/client/client_cli.c b/src/client/client_cli.c index dfe39ee..4341c42 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -77,6 +77,18 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt return 0; } +/* Set and validate the compression algorithm selected by the client. */ +static int set_compression_choice(Config* config, const char* value) { + if (strcmp(value, "zstd") != 0 && strcmp(value, "none") != 0) { + log_message(LOG_LEVEL_ERROR, "--compress-choice must be zstd or none"); + return -1; + } + if (set_string_option(&config->compress_choice, value, "--compress-choice") != 0) + return -1; + config->use_compression = strcmp(value, "zstd") == 0; + return 0; +} + /* Parse a string as a non-negative integer into *dest. Returns true on success, false on error. */ static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) { if (!parse_nonneg_int(value, dest)) { @@ -165,6 +177,7 @@ static const OptionEntry OPTION_TABLE[] = { {"--partial-dir", NULL, OPT_STRING, offsetof(Config, partial_dir)}, {"--suffix", NULL, OPT_STRING, offsetof(Config, suffix)}, {"--compress-choice", "--zc", OPT_STRING, offsetof(Config, compress_choice)}, + {"--compress-level", "--zl", OPT_POS_INT, offsetof(Config, compression_level)}, {"--timeout", NULL, OPT_POS_INT, offsetof(Config, timeout)}, {"--contimeout", NULL, OPT_POS_INT, offsetof(Config, contimeout)}, @@ -185,6 +198,26 @@ static const OptionEntry* find_table_option(const char* arg) { return NULL; } +static const OptionEntry* find_table_option_with_equals(const char* arg, const char** value) { + const char* equals = strchr(arg, '='); + if (!equals || equals == arg) + return NULL; + size_t name_len = (size_t)(equals - arg); + for (size_t i = 0; i < sizeof(OPTION_TABLE) / sizeof(OPTION_TABLE[0]); i++) { + const OptionEntry* entry = &OPTION_TABLE[i]; + if ((strlen(entry->name) == name_len && strncmp(arg, entry->name, name_len) == 0) || + (entry->alias && strlen(entry->alias) == name_len && + strncmp(arg, entry->alias, name_len) == 0)) { + if (strcmp(entry->name, "--compress-choice") == 0 || + strcmp(entry->name, "--compress-level") == 0) { + *value = equals + 1; + return entry; + } + } + } + return NULL; +} + static int apply_table_option(Config* config, const OptionEntry* entry, const char* value) { void* field = (char*)config + entry->offset; switch (entry->kind) { @@ -213,14 +246,30 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count) { for (int i = 1; i < argc; i++) { const OptionEntry* entry = find_table_option(argv[i]); + const char* inline_value = NULL; + if (!entry) + entry = find_table_option_with_equals(argv[i], &inline_value); if (entry) { if (entry->kind != OPT_FLAG) { - if (i + 1 >= argc) { + const char* value = inline_value; + if (!value && i + 1 < argc) + value = argv[++i]; + if (!value) { log_message(LOG_LEVEL_ERROR, "missing argument for %s", entry->name); return -1; } - if (apply_table_option(config, entry, argv[++i]) != 0) - return -1; + if (strcmp(entry->name, "--compress-choice") == 0) { + if (set_compression_choice(config, value) != 0) + return -1; + } else { + if (apply_table_option(config, entry, value) != 0) + return -1; + if (strcmp(entry->name, "--compress-level") == 0 && + (config->compression_level < 1 || config->compression_level > 22)) { + log_message(LOG_LEVEL_ERROR, "--compress-level must be between 1 and 22"); + return -1; + } + } } else if (apply_table_option(config, entry, NULL) != 0) { return -1; } @@ -234,7 +283,8 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, printf("fastsync version %s\n", PROTOCOL_VERSION); return 1; } else if (opt_is(argv[i], "-a", "--archive")) { - config->use_compression = true; + config->use_compression = + !config->compress_choice || strcmp(config->compress_choice, "zstd") == 0; config->use_multithreading = true; config->use_metadata = true; log_message(LOG_LEVEL_INFO, "Enabled archive mode (-c -m -M)"); @@ -270,7 +320,8 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, else log_message(LOG_LEVEL_WARNING, "--delta-max value %llu too small, using default", val); } else if (opt_is(argv[i], "-c", "-z")) { - config->use_compression = true; + config->use_compression = + !config->compress_choice || strcmp(config->compress_choice, "zstd") == 0; log_message(LOG_LEVEL_INFO, "Enabled Compression"); if (i + 1 < argc) { char* end_ptr; @@ -355,13 +406,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (opt_is(argv[i], "-T", NULL) && i + 1 < argc) { if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; - } else if (opt_is(argv[i], "--compress-level", "--zl") && i + 1 < argc) { - if (set_positive_int_option(&config->compression_level, argv[++i], "--compress-level") != 0) - return -1; - if (config->compression_level < 1 || config->compression_level > 22) { - log_message(LOG_LEVEL_ERROR, "--compress-level must be between 1 and 22"); - return -1; - } } else if (argv[i][0] == '-') { fprintf(stderr, "Unknown option: %s\n", argv[i]); print_usage(); @@ -376,6 +420,8 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } } } + if (config->compress_choice) + config->use_compression = strcmp(config->compress_choice, "zstd") == 0; return 0; } diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index 312add4..38b909d 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -52,6 +52,28 @@ class TestArchiveMode: assert not mismatches, f"Mismatch: {mismatches}" +class TestCompressionChoice: + def test_zstd_choice_compresses(self, shared_server): + clean_dir(DEST_DIR) + result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--zc", "zstd"], + port=shared_server.port) + assert result.returncode == 0, f"zstd sync failed: {(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}" + + def test_none_choice_disables_compression(self, shared_server): + clean_dir(DEST_DIR) + result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-z", "--compress-choice", "none"], + port=shared_server.port) + assert result.returncode == 0, f"none sync failed: {(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: def test_exclude_single(self, shared_server): clean_dir(DEST_DIR) diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 9a9d7d1..148e854 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -350,11 +350,36 @@ static void test_parse_args_compression_aliases() { EXPECT_EQ_INT(ret, 0); EXPECT_EQ_STR(cfg->compress_choice, "zstd"); EXPECT_EQ_INT(cfg->compression_level, 10); + EXPECT_TRUE(cfg->use_compression); EXPECT_EQ_INT(positional_count, 2); config_delete(cfg); } +static void test_parse_args_compression_equals_and_none() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-z", "--zc=none", "--zl=7", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 6, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 0); + EXPECT_EQ_STR(cfg->compress_choice, "none"); + EXPECT_EQ_INT(cfg->compression_level, 7); + EXPECT_FALSE(cfg->use_compression); + config_delete(cfg); +} + +static void test_parse_args_rejects_invalid_compression_choice() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--compress-choice=bogus", "/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() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -377,4 +402,6 @@ void test_client_cli() { test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); test_parse_args_compression_aliases(); + test_parse_args_compression_equals_and_none(); + test_parse_args_rejects_invalid_compression_choice(); } From aab45873acea26ab201cd4a78df4961ccdc69fdc Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 22:47:41 +0200 Subject: [PATCH 3/3] test: cover compression option equals forms --- tests/test_client_cli.c | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 148e854..75e6639 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -3,6 +3,7 @@ #include "config.h" #include "test_utils.h" #include "utils.h" +#include #include #include #include @@ -370,6 +371,52 @@ static void test_parse_args_compression_equals_and_none() { config_delete(cfg); } +static void test_parse_args_compression_canonical_equals() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--compress-choice=zstd", "--compress-level=7", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); + EXPECT_EQ_STR(cfg->compress_choice, "zstd"); + EXPECT_EQ_INT(cfg->compression_level, 7); + EXPECT_TRUE(cfg->use_compression); + EXPECT_EQ_INT(positional_count, 2); + + config_delete(cfg); +} + +static void test_parse_args_compression_alias_equals() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--zc=zstd", "--zl=7", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); + EXPECT_EQ_STR(cfg->compress_choice, "zstd"); + EXPECT_EQ_INT(cfg->compression_level, 7); + EXPECT_TRUE(cfg->use_compression); + EXPECT_EQ_INT(positional_count, 2); + + config_delete(cfg); +} + +static void test_parse_args_rejects_invalid_compression_level_equals() { + static const char* const values[] = {"0", "23", "invalid"}; + + for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) { + Config* cfg = config_create(); + char option[32]; + snprintf(option, sizeof(option), "--compress-level=%s", values[i]); + char* argv[] = {"fastsync", option, "/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_rejects_invalid_compression_choice() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--compress-choice=bogus", "/src", "/dst"}; @@ -403,5 +450,8 @@ void test_client_cli() { test_parse_args_archive(); test_parse_args_compression_aliases(); test_parse_args_compression_equals_and_none(); + test_parse_args_compression_canonical_equals(); + test_parse_args_compression_alias_equals(); + test_parse_args_rejects_invalid_compression_level_equals(); test_parse_args_rejects_invalid_compression_choice(); }