diff --git a/README.md b/README.md index fdbd9f3..ec1d079 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,7 @@ defaults to the current directory. | ## Protocol and Security -FastSync protocol version `2.2.0` is shared by the client and server. The +FastSync protocol version `2.2.1` is shared by the client and server. The current protocol is sender-driven and includes configuration negotiation, incremental checks, checksums, manifests, keep-alives, abort handling, and FastSync-native delta messages. Client and server versions must currently diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 9aa4a21..3976f21 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -174,7 +174,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `--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-threads=NUM` | Set compression threads | ❌ Not Implemented | | -| `--skip-compress=LIST` | Skip compress for suffixes | ✅ Implemented | Comma-separated, case-insensitive suffix list; empty list skips none | +| `--skip-compress=LIST` | Skip compress for suffixes | ✅ Implemented | Comma-separated, case-insensitive suffix list; empty list skips none; incompatible with FastSync chunk serialization (`-s`) | ## 13. Connectivity @@ -253,7 +253,6 @@ Ranked by user demand, implementation complexity, and interoperability impact: | 7 | `--delay-updates` | Medium | High — atomic updates | | 8 | `--chmod` | Low | Medium — permission flexibility | | 9 | `--executability` / `-E` | Low | Low — simple flag | -| 10 | `--skip-compress` | Low | Medium — performance tuning | --- diff --git a/src/client/client_validation.c b/src/client/client_validation.c index 106580f..665c939 100644 --- a/src/client/client_validation.c +++ b/src/client/client_validation.c @@ -23,6 +23,11 @@ bool validate_config(const Config* config) { log_message(LOG_LEVEL_ERROR, "--incremental is not supported with -s (chunk serialization)"); return false; } + if (config->skip_compress_set && config->use_chunk_serialization) { + log_message(LOG_LEVEL_ERROR, + "--skip-compress cannot be combined with -s (chunk serialization)"); + return false; + } if (config->use_delta && !config->use_incremental) { log_message(LOG_LEVEL_ERROR, "--delta requires --incremental"); return false; diff --git a/src/shared/config.c b/src/shared/config.c index e078ca0..1422ca8 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -132,6 +132,7 @@ static bool validate_received_config(const Config* config) { valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) && + !(config->skip_compress_set && config->use_chunk_serialization) && (!config->use_compression || (config->compression_level >= 1 && config->compression_level <= 22)) && config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE && diff --git a/src/shared/config.h b/src/shared/config.h index 6ce70eb..e42223e 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -132,7 +132,7 @@ typedef struct Config { bool skip_compress_set; } Config; -#define PROTOCOL_VERSION "2.2.0" +#define PROTOCOL_VERSION "2.2.1" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) Config* config_create(void); diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index 980545a..fff2dac 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -459,7 +459,10 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) { return NULL; } - if (config->use_compression) { + if (config->use_compression && + !compression_should_skip_with_suffixes(file->path, config->skip_compress_suffixes, + config->skip_compress_set ? config->skip_compress_count + : -1)) { Data* uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_FILE_SIZE); data_destroy(file_data); if (uncompressed == NULL) { diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index 6ed9454..43a7765 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -80,6 +80,35 @@ class TestSkipCompress: assert not missing, f"Missing: {missing}" assert not mismatches, f"Mismatch: {mismatches}" + def test_skip_compress_incremental_full_fallback(self, shared_server): + clean_dir(DEST_DIR) + path = os.path.join(SOURCE_DIR, "incremental-skip.TXT") + with open(path, "wb") as f: + f.write(b"original skipped content\n") + flags = ["-c", "-M", "--skip-compress=.txt"] + result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port) + assert result.returncode == 0, f"Initial sync failed: {(result.stderr or result.stdout)[:200]}" + with open(path, "wb") as f: + f.write(b"updated skipped content\n") + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=flags + ["--incremental"], + port=shared_server.port, + ) + assert result.returncode == 0, f"Incremental sync failed: {(result.stderr or result.stdout)[:200]}" + received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) + with open(os.path.join(received, "incremental-skip.TXT"), "rb") as f: + assert f.read() == b"updated skipped content\n" + + def test_skip_compress_rejects_chunk_serialization(self, shared_server): + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-c", "-s", "--skip-compress=.txt"], + port=shared_server.port, + ) + assert result.returncode != 0 + assert "cannot be combined" in (result.stderr or result.stdout) + class TestExclude: def test_exclude_single(self, shared_server): diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 31261b7..88f5519 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -36,6 +36,9 @@ static void test_validate_config_incompatible_options() { cfg->use_incremental = true; cfg->use_chunk_serialization = true; EXPECT_FALSE(validate_config(cfg)); + cfg->use_incremental = false; + cfg->skip_compress_set = true; + EXPECT_FALSE(validate_config(cfg)); config_delete(cfg); } diff --git a/tests/test_config.c b/tests/test_config.c index 4f124db..1f0f953 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -121,11 +121,15 @@ static void test_config_send_receive() { send_cfg->receive_root_directory = str_dup("/send/dst"); send_cfg->save_to_disk = true; send_cfg->use_multithreading = true; - send_cfg->use_chunk_serialization = true; + send_cfg->use_chunk_serialization = false; send_cfg->use_compression = true; send_cfg->use_metadata = true; send_cfg->compression_level = 5; send_cfg->chunk_size = 1024; + send_cfg->skip_compress_set = true; + send_cfg->skip_compress_count = 1; + send_cfg->skip_compress_suffixes = calloc(1, sizeof(char*)); + send_cfg->skip_compress_suffixes[0] = str_dup(".zip"); /* Use socketpair for bidirectional communication */ int p[2]; @@ -154,12 +158,15 @@ static void test_config_send_receive() { ok = false; if (!recv_cfg->use_multithreading) ok = false; - if (!recv_cfg->use_chunk_serialization) + if (recv_cfg->use_chunk_serialization) ok = false; if (recv_cfg->compression_level != 5) ok = false; if (recv_cfg->chunk_size != 1024) ok = false; + if (!recv_cfg->skip_compress_set || recv_cfg->skip_compress_count != 1 || + strcmp(recv_cfg->skip_compress_suffixes[0], ".zip") != 0) + ok = false; } config_delete(recv_cfg); close(p[0]);