From 09fca8d93147f0a36fa597f4db8d8ce63aa5f39e Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 11 Aug 2026 20:26:15 +0200 Subject: [PATCH 1/5] start of human refactoring --- src/client/client_cli.c | 165 ++++++---------------------------------- src/client/usage.c | 113 +++++++++++++++++++++++++++ src/shared/log.c | 27 ++++--- 3 files changed, 152 insertions(+), 153 deletions(-) create mode 100644 src/client/usage.c diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 711a449..3181675 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -8,9 +8,11 @@ #include "utils.h" #include #include +#include #include #include #include +#include "usage.c" #ifndef FASTSYNC_TEST_BUILD /* Parse environment variables for source/destination directories and save-to-disk flag. */ @@ -25,19 +27,6 @@ static void parse_environment(const char** out_env_source, const char** out_env_ } #endif -/* Parse a string as a positive integer, returning true on success. */ -static bool parse_positive_int(const char* s, int* out_val) { - if (!s || *s == '\0') - return false; - char* endptr; - errno = 0; - long val = strtol(s, &endptr, 10); - if (errno != 0 || *endptr != '\0' || val <= 0 || val > INT_MAX) - return false; - *out_val = (int)val; - return true; -} - /* Parse a string as a non-negative integer, returning true on success. */ static bool parse_nonneg_int(const char* s, int* out_val) { if (!s || *s == '\0') @@ -51,9 +40,22 @@ static bool parse_nonneg_int(const char* s, int* out_val) { return true; } -/* Duplicate a string argument into *dest, freeing the old value. Returns 0 on success, -1 on +/* Parse a string as a positive integer, returning true on success. */ +static bool parse_positive_int(const char* s, int* out_val) { + int temp; + if (!parse_nonneg_int(s, &temp)) { + return false; + } + if (temp == 0) { + return false; + } + *out_val = temp; + return true; +} + +/* Duplicate a string argument into *dest, freeing the old value. Returns true on success, false on * failure. */ -static int set_string_option(char** dest, const char* value, const char* option_name) { +static bool set_string_option(char** dest, const char* value, const char* option_name) { char* dup = str_dup(value); if (!dup) { fprintf(stderr, "Error: memory allocation failed for %s\n", option_name); @@ -64,8 +66,8 @@ static int set_string_option(char** dest, const char* value, const char* option_ return 0; } -/* Parse a string as a positive integer into *dest. Returns 0 on success, -1 on error. */ -static int set_positive_int_option(int* dest, const char* value, const char* option_name) { +/* Parse a string as a positive integer into *dest. Returns true on success, false on error. */ +static bool set_positive_int_option(int* dest, const char* value, const char* option_name) { if (!parse_positive_int(value, dest)) { fprintf(stderr, "Error: %s must be a positive integer\n", option_name); return -1; @@ -73,8 +75,8 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt return 0; } -/* Parse a string as a non-negative integer into *dest. Returns 0 on success, -1 on error. */ -static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) { +/* Parse a string as a non-negative integer into *dest. Returns true on success, false on error. */ +static bool set_nonneg_int_option(int* dest, const char* value, const char* option_name) { if (!parse_nonneg_int(value, dest)) { fprintf(stderr, "Error: %s must be a non-negative integer\n", option_name); return -1; @@ -82,7 +84,6 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio return 0; } -static void print_usage(void); static int read_patterns_from_file(const char* filepath, char*** patterns, int* count); /* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */ @@ -103,12 +104,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) { config->dry_run = true; } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { - if (!parse_positive_int(argv[++i], &config->ssh_port)) { - fprintf(stderr, "Error: invalid --port/-p value: %s\n", argv[i]); + if (!set_positive_int_option(&config->ssh_port, argv[++i], "-p")) return -1; - } if (config->ssh_port > 65535) { - fprintf(stderr, "Error: SSH port must be 1-65535\n"); + log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535\n"); return -1; } } else if (strcmp(argv[i], "--delete") == 0) { @@ -140,14 +139,8 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } config->include_patterns[config->include_count++] = dup; } else if (strcmp(argv[i], "--max-size") == 0 && i + 1 < argc) { - char* end; - errno = 0; - unsigned long long val = strtoull(argv[++i], &end, 10); - if (errno != 0 || *end != '\0') { - fprintf(stderr, "Error: --max-size must be a non-negative integer\n"); + if (!set_nonneg_int_option(&config->max_size, argv[++i], "Max Size")) return -1; - } - config->max_size = val; } else if (strcmp(argv[i], "--min-size") == 0 && i + 1 < argc) { char* end; errno = 0; @@ -507,116 +500,6 @@ static bool validate_config(const Config* config) { } #endif /* FASTSYNC_TEST_BUILD */ -static void print_usage(void) { - printf("Usage:\n"); - printf(" fastsync [options] \n"); - printf(" fastsync [options] --source-dir --dest-dir \n"); - printf("\n"); - printf("Destination formats:\n"); - printf(" user@host:/path SSH transport (rsync-style)\n"); - printf(" host:/path SSH transport (current user)\n"); - printf(" /local/path TCP transport (requires server on localhost:8080)\n"); - printf("\n"); - printf("Options:\n"); - printf(" -c [level] Enable compression (level 1-22, default 5)\n"); - printf(" -z [level] Alias for -c\n"); - printf(" -a, --archive Archive mode (-c -m -M)\n"); - printf(" -n, --dry-run Show what would be transferred\n"); - printf(" -p SSH port (default: 22)\n"); - printf(" --progress Show transfer progress\n"); - printf(" --delete Delete files on receiver not in source\n"); - printf(" --exclude Exclude files matching pattern\n"); - printf(" --include Only include files matching pattern\n"); - printf(" --exclude-from Read exclude patterns from file\n"); - printf(" --include-from Read include patterns from file\n"); - printf(" --max-size Skip files larger than n bytes\n"); - printf(" --min-size Skip files smaller than n bytes\n"); - printf(" --incremental Skip files unchanged since last transfer\n"); - printf(" --delta Delta transfer for changed files (requires --incremental)\n"); - printf(" --delta-block Delta block size in bytes (default: %d)\n", - DELTA_BLOCK_SIZE_DEFAULT); - printf(" --delta-max Max file size for delta transfer (default: %llu)\n", - DELTA_MAX_FILE_SIZE); - printf(" -m Enable multithreading\n"); - printf(" -s Enable chunk serialization\n"); - printf(" -f Enable sendfile (TCP only, not with -c or -s)\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); - printf(" --source-dir Source directory\n"); - printf(" --dest-dir Destination directory\n"); - printf(" --save-to-disk Write received files to disk\n"); - printf(" --server-host Server IP address (default: 127.0.0.1)\n"); - printf(" --server-port Server port (default: 8080)\n"); - printf(" --bwlimit Bandwidth limit in kilobytes per second\n"); - printf(" --tls Enable TLS encryption\n"); - printf(" --cert TLS certificate file (PEM)\n"); - printf(" --key TLS private key file (PEM)\n"); - printf(" --ca TLS CA certificate file (PEM)\n"); - printf(" --timeout I/O timeout in seconds (default: 30)\n"); - printf(" -T Alias for --timeout\n"); - printf(" --contimeout Connection timeout in seconds (default: 10)\n"); - printf(" --address Server hostname/IP to connect to\n"); - printf(" --bind-address Bind to specific local address\n"); - printf(" --ipv6 Prefer IPv6 connections\n"); - printf(" --ipv4 Prefer IPv4 connections\n"); - printf(" -q, --quiet Suppress non-error output\n"); - printf(" --silent Alias for --quiet\n"); - printf(" --backup Backup existing files before overwriting\n"); - printf(" --backup-dir Directory for backups (requires --backup)\n"); - printf(" --suffix Backup suffix (default: ~)\n"); - printf(" --stats Print transfer statistics at end\n"); - printf(" --max-depth Maximum directory depth (0=unlimited)\n"); - printf(" --log-file Write log messages to file\n"); - printf(" --queue-size Queue capacity for multithreaded mode (default: 100)\n"); - printf(" --partial Keep partial files on interrupted transfer\n"); - printf(" --partial-dir Directory for partial files\n"); - printf(" --fastsync-server-path \n"); - printf(" Path to fastsync-server on remote (default: fastsync-server)\n"); - printf(" -l, --links Copy symlinks as symlinks\n"); - printf(" --copy-links Transform symlinks into referent files\n"); - printf(" --safe-links Skip symlinks that point outside transfer tree\n"); - printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n"); - printf(" -H, --hard-links Preserve hard links\n"); - printf(" -A, --acls Preserve ACLs\n"); - printf(" -X, --xattrs Preserve extended attributes\n"); - printf(" -D, --devices Preserve device files\n"); - printf(" -S, --sparse Handle sparse files efficiently\n"); - printf(" -i, --itemize-changes Show per-file change summary\n"); - printf(" --out-format Custom output format string\n"); - printf(" --info Info verbosity level\n"); - printf(" --debug Debug verbosity level\n"); - printf(" --list-only List files without transferring\n"); - printf(" -h, --human-readable Human-readable numbers\n"); - printf(" -u, --update Skip files newer on destination\n"); - printf(" --inplace Update files in-place (no temp+rename)\n"); - printf(" --append Append data to shorter files\n"); - printf(" --append-verify Append with verify\n"); - printf(" --delete-before Delete before transfer\n"); - printf(" --delete-excluded Also delete excluded files\n"); - printf(" --delete-after Delete after transfer, not before\n"); - printf(" --max-delete Maximum number of files to delete\n"); - printf(" --filter Add file filtering rule\n"); - printf(" --files-from Read file list from file\n"); - printf(" --cvs-exclude Auto-ignore CVS files\n"); - printf(" --prune-empty-dirs Omit empty directories from transfer\n"); - printf(" -R, --relative Use relative paths\n"); - printf(" -e, --rsh Specify remote shell\n"); - printf(" --rsync-path Path to remote binary\n"); - printf(" --daemon Run in daemon mode\n"); - printf(" --config Path to configuration file\n"); - printf(" --server Run in server mode\n"); - printf(" --checksum Skip files based on checksum, not mod-time/size\n"); - printf(" --compress-choice Compression algorithm (default: zstd)\n"); - printf(" --compress-level Compression level (default: 5)\n"); - printf(" --temp-dir Temporary directory for files\n"); - printf(" --compare-dest Compare destination\n"); - printf(" --copy-dest Copy destination\n"); - printf(" --link-dest Link destination\n"); - printf(" --help Show this help\n"); - printf(" -V, --version Show version\n"); -} - static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) { FILE* fp = fopen(filepath, "r"); if (!fp) { diff --git a/src/client/usage.c b/src/client/usage.c new file mode 100644 index 0000000..2c477ea --- /dev/null +++ b/src/client/usage.c @@ -0,0 +1,113 @@ +#include "stdio.h" +#include +#include + +static __attribute__((unused)) void print_usage() { + printf("Usage:\n"); + printf(" fastsync [options] \n"); + printf(" fastsync [options] --source-dir --dest-dir \n"); + printf("\n"); + printf("Destination formats:\n"); + printf(" user@host:/path SSH transport (rsync-style)\n"); + printf(" host:/path SSH transport (current user)\n"); + printf(" /local/path TCP transport (requires server on localhost:8080)\n"); + printf("\n"); + printf("Options:\n"); + printf(" -c [level] Enable compression (level 1-22, default 5)\n"); + printf(" -z [level] Alias for -c\n"); + printf(" -a, --archive Archive mode (-c -m -M)\n"); + printf(" -n, --dry-run Show what would be transferred\n"); + printf(" -p SSH port (default: 22)\n"); + printf(" --progress Show transfer progress\n"); + printf(" --delete Delete files on receiver not in source\n"); + printf(" --exclude Exclude files matching pattern\n"); + printf(" --include Only include files matching pattern\n"); + printf(" --exclude-from Read exclude patterns from file\n"); + printf(" --include-from Read include patterns from file\n"); + printf(" --max-size Skip files larger than n bytes\n"); + printf(" --min-size Skip files smaller than n bytes\n"); + printf(" --incremental Skip files unchanged since last transfer\n"); + printf(" --delta Delta transfer for changed files (requires --incremental)\n"); + printf(" --delta-block Delta block size in bytes (default: %d)\n", + DELTA_BLOCK_SIZE_DEFAULT); + printf(" --delta-max Max file size for delta transfer (default: %llu)\n", + DELTA_MAX_FILE_SIZE); + printf(" -m Enable multithreading\n"); + printf(" -s Enable chunk serialization\n"); + printf(" -f Enable sendfile (TCP only, not with -c or -s)\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); + printf(" --source-dir Source directory\n"); + printf(" --dest-dir Destination directory\n"); + printf(" --save-to-disk Write received files to disk\n"); + printf(" --server-host Server IP address (default: 127.0.0.1)\n"); + printf(" --server-port Server port (default: 8080)\n"); + printf(" --bwlimit Bandwidth limit in kilobytes per second\n"); + printf(" --tls Enable TLS encryption\n"); + printf(" --cert TLS certificate file (PEM)\n"); + printf(" --key TLS private key file (PEM)\n"); + printf(" --ca TLS CA certificate file (PEM)\n"); + printf(" --timeout I/O timeout in seconds (default: 30)\n"); + printf(" -T Alias for --timeout\n"); + printf(" --contimeout Connection timeout in seconds (default: 10)\n"); + printf(" --address Server hostname/IP to connect to\n"); + printf(" --bind-address Bind to specific local address\n"); + printf(" --ipv6 Prefer IPv6 connections\n"); + printf(" --ipv4 Prefer IPv4 connections\n"); + printf(" -q, --quiet Suppress non-error output\n"); + printf(" --silent Alias for --quiet\n"); + printf(" --backup Backup existing files before overwriting\n"); + printf(" --backup-dir Directory for backups (requires --backup)\n"); + printf(" --suffix Backup suffix (default: ~)\n"); + printf(" --stats Print transfer statistics at end\n"); + printf(" --max-depth Maximum directory depth (0=unlimited)\n"); + printf(" --log-file Write log messages to file\n"); + printf(" --queue-size Queue capacity for multithreaded mode (default: 100)\n"); + printf(" --partial Keep partial files on interrupted transfer\n"); + printf(" --partial-dir Directory for partial files\n"); + printf(" --fastsync-server-path \n"); + printf(" Path to fastsync-server on remote (default: fastsync-server)\n"); + printf(" -l, --links Copy symlinks as symlinks\n"); + printf(" --copy-links Transform symlinks into referent files\n"); + printf(" --safe-links Skip symlinks that point outside transfer tree\n"); + printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n"); + printf(" -H, --hard-links Preserve hard links\n"); + printf(" -A, --acls Preserve ACLs\n"); + printf(" -X, --xattrs Preserve extended attributes\n"); + printf(" -D, --devices Preserve device files\n"); + printf(" -S, --sparse Handle sparse files efficiently\n"); + printf(" -i, --itemize-changes Show per-file change summary\n"); + printf(" --out-format Custom output format string\n"); + printf(" --info Info verbosity level\n"); + printf(" --debug Debug verbosity level\n"); + printf(" --list-only List files without transferring\n"); + printf(" -h, --human-readable Human-readable numbers\n"); + printf(" -u, --update Skip files newer on destination\n"); + printf(" --inplace Update files in-place (no temp+rename)\n"); + printf(" --append Append data to shorter files\n"); + printf(" --append-verify Append with verify\n"); + printf(" --delete-before Delete before transfer\n"); + printf(" --delete-excluded Also delete excluded files\n"); + printf(" --delete-after Delete after transfer, not before\n"); + printf(" --max-delete Maximum number of files to delete\n"); + printf(" --filter Add file filtering rule\n"); + printf(" --files-from Read file list from file\n"); + printf(" --cvs-exclude Auto-ignore CVS files\n"); + printf(" --prune-empty-dirs Omit empty directories from transfer\n"); + printf(" -R, --relative Use relative paths\n"); + printf(" -e, --rsh Specify remote shell\n"); + printf(" --rsync-path Path to remote binary\n"); + printf(" --daemon Run in daemon mode\n"); + printf(" --config Path to configuration file\n"); + printf(" --server Run in server mode\n"); + printf(" --checksum Skip files based on checksum, not mod-time/size\n"); + printf(" --compress-choice Compression algorithm (default: zstd)\n"); + printf(" --compress-level Compression level (default: 5)\n"); + printf(" --temp-dir Temporary directory for files\n"); + printf(" --compare-dest Compare destination\n"); + printf(" --copy-dest Copy destination\n"); + printf(" --link-dest Link destination\n"); + printf(" --help Show this help\n"); + printf(" -V, --version Show version\n"); +} diff --git a/src/shared/log.c b/src/shared/log.c index fbd9400..c22a853 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -15,6 +15,15 @@ void log_set_file(FILE* fp) { log_fp = fp; } +static inline void write_message(FILE* dest_io, LogLevel log_level, struct tm t, const char* format, + va_list args) { + fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, + t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); + + vfprintf(dest_io, format, args); + fprintf(dest_io, "\n"); +} + void log_message(LogLevel log_level, const char* format, ...) { if (log_level < current_log_level) return; @@ -25,22 +34,16 @@ void log_message(LogLevel log_level, const char* format, ...) { if (!localtime_r(&now, &t)) return; - fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, - t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); + FILE* dest_io = stdout; + if (log_level == LOG_LEVEL_ERROR) { + dest_io = stderr; + } va_list args; va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); - fprintf(stderr, "\n"); + write_message(dest_io, log_level, t, format, args); if (log_fp) { - fprintf(log_fp, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, - t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); - va_start(args, format); - vfprintf(log_fp, format, args); - va_end(args); - fprintf(log_fp, "\n"); - fflush(log_fp); + write_message(log_fp, log_level, t, format, args); } } From d146e1bb7c9bbb7ae63a1f1b268e8a70e423aa1f Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 11 Aug 2026 20:59:06 +0200 Subject: [PATCH 2/5] Remove unimplemented CLI options --- README.md | 16 ++--- RSYNC_COMPAT.md | 85 ++++++++++++------------- src/client/client_cli.c | 115 +++------------------------------ src/client/client_send.c | 7 +- src/client/scanner.c | 9 +-- src/client/scanner.h | 5 +- src/client/usage.c | 40 ------------ src/shared/config.c | 134 ++++++++++----------------------------- src/shared/config.h | 59 ----------------- tests/test_client_cli.c | 29 +++++++++ tests/test_scanner.c | 24 +++---- 11 files changed, 138 insertions(+), 385 deletions(-) diff --git a/README.md b/README.md index 353133c..7d3ff19 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,10 @@ A high-performance file synchronization system with SSH and TCP transport, TLS e - Optional progress display with throughput - Bandwidth limiting via token-bucket algorithm - Configurable I/O and connection timeouts (`--timeout`, `--contimeout`) -- Quiet mode (`-q`/`--quiet`) suppresses all non-error output - Backup overwritten files (`--backup`) with optional directory (`--backup-dir`) - Transfer statistics summary (`--stats`) - Maximum directory depth control (`--max-depth`) - Log file output (`--log-file`) -- Configurable multithreaded queue size (`--queue-size`) - Exclude patterns from file (`--exclude-from`) ### Server @@ -114,8 +112,6 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up | `-n, --dry-run` | Scan and print what would be transferred | | `-p ` | SSH port (default: 22) | | `-v, --verbose` | Enable debug logging | -| `-q, --quiet` | Suppress all non-error output | -| `--silent` | Alias for `--quiet` | | `--progress` | Show real-time transfer speed | | `--delete` | Delete files on receiver not present in source | | `--exclude ` | Exclude files matching glob pattern (repeatable) | @@ -133,7 +129,6 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up | `--stats` | Print transfer statistics at end (bytes, files, timing) | | `--max-depth ` | Maximum directory depth to recurse (0 = unlimited, default: 0) | | `--log-file ` | Write log messages to file instead of stderr | -| `--queue-size ` | Queue capacity for multithreaded mode (default: 100) | | `--source-dir ` | Source directory (overrides `FASTSYNC_SOURCE_DIR`) | | `--dest-dir ` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) | | `--save-to-disk` | Write received files to disk | @@ -177,7 +172,7 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up 1. **Chunk** — collection of files (~10 MB total by default) 2. **File** — path, content (`Data`), optional `FileMetadata` pointer 3. **FileMetadata** — `mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec` -4. **Config** — runtime parameters (transported over wire, TLS settings excluded). Includes `timeout`, `contimeout`, `quiet`, `backup`, `backup_dir`, `stats`, `max_depth`, `log_file`, `queue_size`. +4. **Config** — runtime parameters (transported over wire, TLS settings excluded). Includes `timeout`, `contimeout`, `backup`, `backup_dir`, `stats`, and `max_depth`. 5. **Queue** — thread-safe bounded queue with condition variables 6. **DirectoryScanner** — recursive BFS traversal with exclude and include pattern support, max-depth enforcement @@ -294,8 +289,8 @@ Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh u # Bandwidth limit to 1 MB/s ./build/client --bwlimit 1024 /src user@host:/dst -# With timeouts, quiet mode, and stats -./build/client --timeout 60 --contimeout 15 --quiet --stats /src user@host:/dst +# With timeouts and stats +./build/client --timeout 60 --contimeout 15 --stats /src user@host:/dst # Backup overwritten files to a directory ./build/client --backup --backup-dir /backups /src user@host:/dst @@ -303,9 +298,6 @@ Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh u # Exclude patterns from file, limit depth ./build/client --exclude-from ignore.txt --max-depth 3 /src user@host:/dst -# Custom queue size for multithreading -./build/client -m --queue-size 200 /src user@host:/dst - # Log to file ./build/client --log-file /tmp/fastsync.log /src user@host:/dst @@ -332,7 +324,7 @@ The benchmark prints throughput metrics, best configuration, and speedup vs rsyn 1. Chunk size (~10 MB default) balances memory and transfer efficiency 2. Compression level trades CPU for bandwidth 3. `sendfile()` bypasses userspace — ~2× faster on localhost for large files -4. Multithreading scales with core count; `--queue-size` controls pipeline buffering +4. Multithreading scales with core count and uses memory-based pipeline sizing 5. Metadata transfer adds negligible overhead (~24 bytes per file when enabled) 6. SSH socketpair buffer set to 1 MB for improved pipe throughput 7. SSH ControlMaster reuses connections across repeated invocations diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index e9f6dff..65a265b 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -6,10 +6,10 @@ This document maps rsync's full feature set to FastSync's current implementation | Status | Count | Description | |--------|-------|-------------| -| ✅ Implemented | 39 | Feature works end-to-end | -| 🔀 Alt Arg | 5 | Functionality exists but under different flag/semantics | -| ⚠️ Partial | 30 | Flag parsed/stored but behavior incomplete | -| ❌ Not Implemented | 62 | Flag not recognized or no behavior | +| ✅ Implemented | 34 | Feature works end-to-end | +| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics | +| ⚠️ Partial | 1 | Flag parsed/stored but behavior incomplete | +| ❌ Not Implemented | 98 | Flag not recognized or no behavior | | **Total** | **136** | | --- @@ -20,31 +20,31 @@ This document maps rsync's full feature set to FastSync's current implementation |------|-------------------|-----------------|-------| | `-a`, `--archive` | Archive mode is -rlptgoD | 🔀 Alt Arg | Maps to -c -m -M (compression + multithread + metadata) | | `-v`, `--verbose` | Increase verbosity | ✅ Implemented | Sets `log_level=DEBUG` | -| `-q`, `--quiet` | Suppress non-error messages | ✅ Implemented | `quiet` config field | -| `-h`, `--help` | Show help | ✅ Implemented | Prints usage and exits | -| `-V`, `--version` | Print version | ❌ Not Implemented | | -| `--info=FLAGS` | Fine-grained info verbosity | ⚠️ Partial | `info_level` stored, not wired | -| `--debug=FLAGS` | Fine-grained debug verbosity | ⚠️ Partial | `debug_level` stored, not wired | +| `-q`, `--quiet` | Suppress non-error messages | ❌ Not Implemented | Removed because it had no effect | +| `--help` | Show help | ✅ Implemented | Prints usage and exits; `-h` is not accepted | +| `-V`, `--version` | Print version | ✅ Implemented | | +| `--info=FLAGS` | Fine-grained info verbosity | ❌ Not Implemented | Removed because it had no effect | +| `--debug=FLAGS` | Fine-grained debug verbosity | ❌ Not Implemented | Removed because it had no effect | | `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | | | `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | | | `--exclude=PATTERN` | Exclude files matching pattern | ✅ Implemented | Glob matching in scanner | | `--include=PATTERN` | Include files matching pattern | ✅ Implemented | Glob matching in scanner | -| `-C`, `--cvs-exclude` | Auto-ignore CVS files | ⚠️ Partial | `cvs_exclude` stored, not wired | +| `-C`, `--cvs-exclude` | Auto-ignore CVS files | ❌ Not Implemented | Removed because it had no effect | ## 2. Modifying Output | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `--stats` | Give transfer stats | ✅ Implemented | Prints file/byte counts | -| `-h`, `--human-readable` | Human-readable numbers | ⚠️ Partial | `human_readable` stored, not wired | -| `-i`, `--itemize-changes` | Per-file change summary | ⚠️ Partial | `itemize_changes` stored, not wired | +| `-h`, `--human-readable` | Human-readable numbers | ❌ Not Implemented | Removed because it had no effect | +| `-i`, `--itemize-changes` | Per-file change summary | ❌ Not Implemented | Removed because it had no effect | | `--progress` | Show progress | ✅ Implemented | Progress callback in sender | | `-P` | Same as --partial --progress | ❌ Not Implemented | | -| `--out-format=FORMAT` | Custom output format | ⚠️ Partial | `out_format` stored, not wired | +| `--out-format=FORMAT` | Custom output format | ❌ Not Implemented | Removed because it had no effect | | `--log-file=FILE` | Log to file | ✅ Implemented | `log_file` config field | | `--log-file-format=FMT` | Log format | ❌ Not Implemented | | | `--8-bit-output` | Leave high-bit chars unescaped | ❌ Not Implemented | | -| `--list-only` | List files instead of copying | ⚠️ Partial | `list_only` stored, not wired | +| `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect | ## 3. File Selection @@ -52,8 +52,8 @@ This document maps rsync's full feature set to FastSync's current implementation |------|-------------------|-----------------|-------| | `--exclude-from=FILE` | Read exclude patterns from file | ✅ Implemented | Reads patterns from file | | `--include-from=FILE` | Read include patterns from file | ✅ Implemented | Reads patterns from file | -| `--filter=RULE` | Add file-filtering rule | ⚠️ Partial | `filters` ArrayList stored, not wired | -| `--files-from=FILE` | Read source file list from file | ⚠️ Partial | `files_from` stored, not wired | +| `--filter=RULE` | Add file-filtering rule | ❌ Not Implemented | Removed because it had no effect | +| `--files-from=FILE` | Read source file list from file | ❌ Not Implemented | Removed because it had no effect | | `-0`, `--from0` | Delimit *-from files with NULs | ❌ Not Implemented | | | `--max-size=SIZE` | Skip files larger than SIZE | ✅ Implemented | `max_size` in scanner | | `--min-size=SIZE` | Skip files smaller than SIZE | ✅ Implemented | `min_size` in scanner | @@ -69,7 +69,7 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `-r`, `--recursive` | Recurse into directories | ✅ Implemented | Default behavior | -| `-R`, `--relative` | Use relative path names | ⚠️ Partial | `relative` stored, not wired | +| `-R`, `--relative` | Use relative path names | ❌ Not Implemented | Removed because it had no effect | | `--no-implied-dirs` | Don't send implied dirs with -R | ❌ Not Implemented | | | `-d`, `--dirs` | Transfer dirs without recursing | ❌ Not Implemented | | | `--mkpath` | Create missing path components | ❌ Not Implemented | | @@ -78,10 +78,10 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `-u`, `--update` | Skip files newer on receiver | ⚠️ Partial | `update` stored, not wired | +| `-u`, `--update` | Skip files newer on receiver | ❌ Not Implemented | Removed because it had no effect | | `--inplace` | Update files in-place | ✅ Implemented | Direct write mode | -| `--append` | Append data to shorter files | ⚠️ Partial | `append` stored, not wired | -| `--append-verify` | Append with old-data checksum | ⚠️ Partial | `append_verify` stored, not wired | +| `--append` | Append data to shorter files | ❌ Not Implemented | Removed because it had no effect | +| `--append-verify` | Append with old-data checksum | ❌ Not Implemented | Removed because it had no effect | | `-W`, `--whole-file` | Copy whole file (no delta) | ❌ Not Implemented | | | `--block-size=SIZE` | Force checksum block-size | ⚠️ Partial | Parsed as `--delta-block`; controls delta transfer block size | @@ -100,15 +100,15 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field | -| `--delete-before` | Delete before transfer | ⚠️ Partial | `delete_before` stored, not wired | +| `--delete-before` | Delete before transfer | ❌ Not Implemented | Removed because it had no effect | | `--delete-during` | Delete during transfer | ❌ Not Implemented | | | `--delete-delay` | Find deletions during, delete after | ❌ Not Implemented | | -| `--delete-after` | Delete after transfer | ⚠️ Partial | `delete_after` stored, not wired | -| `--delete-excluded` | Also delete excluded files | ⚠️ Partial | `delete_excluded` stored, not wired | -| `--max-delete=NUM` | Max files to delete | ⚠️ Partial | `max_delete` stored, not wired | +| `--delete-after` | Delete after transfer | ❌ Not Implemented | Removed because it had no effect | +| `--delete-excluded` | Also delete excluded files | ❌ Not Implemented | Removed because it had no effect | +| `--max-delete=NUM` | Max files to delete | ❌ Not Implemented | Removed because it had no effect | | `--ignore-errors` | Delete even with I/O errors | ❌ Not Implemented | | | `--force` | Force deletion of non-empty dirs | ❌ Not Implemented | | -| `--prune-empty-dirs` | Prune empty dir chains | ⚠️ Partial | `prune_empty_dirs` stored, not wired | +| `--prune-empty-dirs` | Prune empty dir chains | ❌ Not Implemented | Removed because it had no effect | ## 8. Metadata Preservation @@ -121,11 +121,11 @@ This document maps rsync's full feature set to FastSync's current implementation | `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M | | `-E`, `--executability` | Preserve executability | ❌ Not Implemented | | | `--chmod=CHMOD` | Affect file permissions | ❌ Not Implemented | | -| `-A`, `--acls` | Preserve ACLs | ⚠️ Partial | `preserve_acls` stored, not wired | -| `-X`, `--xattrs` | Preserve extended attributes | ⚠️ Partial | `preserve_xattrs` stored, not wired | -| `-H`, `--hard-links` | Preserve hard links | ⚠️ Partial | `preserve_hard_links` stored, not wired | -| `-D` | Same as --devices --specials | 🔀 Alt Arg | Maps to --devices only (no --specials) | -| `--devices` | Preserve device files | ⚠️ Partial | `preserve_devices` stored, not wired | +| `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect | +| `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect | +| `-H`, `--hard-links` | Preserve hard links | ❌ Not Implemented | Removed because it had no effect | +| `-D` | Same as --devices --specials | ❌ Not Implemented | Removed because device-file handling is not implemented | +| `--devices` | Preserve device files | ❌ Not Implemented | Removed because it had no effect | | `--specials` | Preserve special files | ❌ Not Implemented | | | `--copy-devices` | Copy device contents as file | ❌ Not Implemented | | | `--write-devices` | Write to devices as files | ❌ Not Implemented | | @@ -159,11 +159,11 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `-c`, `--checksum` | Skip based on checksum | 🔀 Alt Arg | `-c` means compression; `--checksum` stored and forwarded to scanner | +| `--checksum` | Skip based on checksum | ❌ Not Implemented | Removed because it had no effect; `-c` means compression | | `--checksum-choice=STR` | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally | -| `--compare-dest=DIR` | Compare dest files relative to DIR | ⚠️ Partial | `compare_dest` stored, not wired | -| `--copy-dest=DIR` | Include copies of unchanged files | ⚠️ Partial | `copy_dest` stored, not wired | -| `--link-dest=DIR` | Hardlink to files when unchanged | ⚠️ Partial | `link_dest` stored, not wired | +| `--compare-dest=DIR` | Compare dest files relative to DIR | ❌ Not Implemented | Removed because it had no effect | +| `--copy-dest=DIR` | Include copies of unchanged files | ❌ Not Implemented | Removed because it had no effect | +| `--link-dest=DIR` | Hardlink to files when unchanged | ❌ Not Implemented | Removed because it had no effect | | `--fuzzy`, `--no-fuzzy` | Find similar file for basis | ❌ Not Implemented | | ## 12. Compression @@ -171,7 +171,7 @@ 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 | ⚠️ Partial | `compress_choice` stored, always zstd | +| `--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 | ❌ Not Implemented | Internal skip for hardcoded types; not user-configurable | @@ -180,22 +180,22 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `-e`, `--rsh=COMMAND` | Remote shell to use | ✅ Implemented | SSH transport support | -| `--rsync-path=PROGRAM` | rsync binary on remote | ✅ Implemented | `rsync_path` config field | +| `-e`, `--rsh=COMMAND` | Remote shell to use | ❌ Not Implemented | Removed; SSH invokes `ssh` directly | +| `--rsync-path=PROGRAM` | rsync binary on remote | ❌ Not Implemented | Removed; use `--fastsync-server-path` | | `--port=PORT` | Alternate daemon port | ✅ Implemented | `server_port` config field | | `--sockopts=OPTIONS` | Custom TCP options | ❌ Not Implemented | | | `--blocking-io` | Use blocking I/O for remote shell | ❌ Not Implemented | | | `--outbuf=N\|L\|B` | Set output buffering | ❌ Not Implemented | | -| `--address=ADDRESS` | Bind address for outgoing socket | ✅ Implemented | `address` config field | -| `-4`, `--ipv4` | Prefer IPv4 | ✅ Implemented | `ipv4` config field | -| `-6`, `--ipv6` | Prefer IPv6 | ✅ Implemented | `ipv6` config field | +| `--address=ADDRESS` | Bind address for outgoing socket | ❌ Not Implemented | Removed because it had no effect | +| `-4`, `--ipv4` | Prefer IPv4 | ❌ Not Implemented | Removed because it had no effect | +| `-6`, `--ipv6` | Prefer IPv6 | ❌ Not Implemented | Removed because it had no effect | ## 14. Daemon Mode | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `--daemon` | Run as rsync daemon | ⚠️ Partial | `daemon` stored, not wired | -| `--config=FILE` | Alternate rsyncd.conf file | ⚠️ Partial | `daemon_config` stored, not wired | +| `--daemon` | Run as rsync daemon | ❌ Not Implemented | Removed because it had no effect | +| `--config=FILE` | Alternate rsyncd.conf file | ❌ Not Implemented | Removed because it had no effect | | `--dparam=OVERRIDE` | Override global daemon config | ❌ Not Implemented | | | `--no-detach` | Don't detach from parent | ❌ Not Implemented | | | `--password-file=FILE` | Read daemon password from file | ❌ Not Implemented | | @@ -266,7 +266,6 @@ Ranked by user demand, implementation complexity, and interoperability impact: | `-f` / `--sendfile` | Zero-copy sendfile() syscall (TCP only) | | `-c [level]` | zstd compression level (1-22) | | `--chunk-size` | Configurable chunk size | -| `--queue-size` | Pipeline queue capacity | | `--tls` | TLS encryption (mutual auth) | | `--fastsync-server-path` | Path to fastsync-server binary | | `--server-host` / `--server-port` | Direct TCP connection | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 3181675..641d437 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -104,7 +104,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) { config->dry_run = true; } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { - if (!set_positive_int_option(&config->ssh_port, argv[++i], "-p")) + if (set_positive_int_option(&config->ssh_port, argv[++i], "-p") != 0) return -1; if (config->ssh_port > 65535) { log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535\n"); @@ -139,8 +139,14 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } config->include_patterns[config->include_count++] = dup; } else if (strcmp(argv[i], "--max-size") == 0 && i + 1 < argc) { - if (!set_nonneg_int_option(&config->max_size, argv[++i], "Max Size")) + char* end; + errno = 0; + unsigned long long val = strtoull(argv[++i], &end, 10); + if (errno != 0 || *end != '\0') { + fprintf(stderr, "Error: --max-size must be a non-negative integer\n"); return -1; + } + config->max_size = val; } else if (strcmp(argv[i], "--min-size") == 0 && i + 1 < argc) { char* end; errno = 0; @@ -268,9 +274,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) { if (set_positive_int_option(&config->contimeout, argv[++i], "--contimeout") != 0) return -1; - } else if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0 || - strcmp(argv[i], "--silent") == 0) { - config->quiet = true; } else if (strcmp(argv[i], "--backup") == 0) { config->backup = true; } else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) { @@ -294,9 +297,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } config->log_file = lf; log_set_file(lf); - } else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) { - if (set_positive_int_option(&config->queue_size, argv[++i], "--queue-size") != 0) - return -1; } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != 0) @@ -321,118 +321,19 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, config->safe_links = true; } else if (strcmp(argv[i], "--copy-unsafe-links") == 0) { config->copy_unsafe_links = true; - } else if (strcmp(argv[i], "-H") == 0 || strcmp(argv[i], "--hard-links") == 0) { - config->preserve_hard_links = true; - } else if (strcmp(argv[i], "-A") == 0 || strcmp(argv[i], "--acls") == 0) { - config->preserve_acls = true; - } else if (strcmp(argv[i], "-X") == 0 || strcmp(argv[i], "--xattrs") == 0) { - config->preserve_xattrs = true; - } else if (strcmp(argv[i], "-D") == 0 || strcmp(argv[i], "--devices") == 0) { - config->preserve_devices = true; } else if (strcmp(argv[i], "-S") == 0 || strcmp(argv[i], "--sparse") == 0) { config->preserve_sparse = true; - } else if (strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--itemize-changes") == 0) { - config->itemize_changes = true; - } else if (strcmp(argv[i], "--out-format") == 0 && i + 1 < argc) { - if (set_string_option(&config->out_format, argv[++i], "--out-format") != 0) - return -1; - } else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) { - if (set_nonneg_int_option(&config->info_level, argv[++i], "--info") != 0) - return -1; - } else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) { - if (set_nonneg_int_option(&config->debug_level, argv[++i], "--debug") != 0) - return -1; - } else if (strcmp(argv[i], "--list-only") == 0) { - config->list_only = true; - } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--human-readable") == 0) { - config->human_readable = true; - } else if (strcmp(argv[i], "-u") == 0 || strcmp(argv[i], "--update") == 0) { - config->update = true; } else if (strcmp(argv[i], "--inplace") == 0) { config->inplace = true; - } else if (strcmp(argv[i], "--append") == 0) { - config->append = true; - } else if (strcmp(argv[i], "--append-verify") == 0) { - config->append_verify = true; - } else if (strcmp(argv[i], "--delete-excluded") == 0) { - config->delete_excluded = true; - } else if (strcmp(argv[i], "--delete-after") == 0) { - config->delete_after = true; - } else if (strcmp(argv[i], "--max-delete") == 0 && i + 1 < argc) { - if (set_nonneg_int_option(&config->max_delete, argv[++i], "--max-delete") != 0) - return -1; - } else if (strcmp(argv[i], "--filter") == 0 && i + 1 < argc) { - if (!config->filters) - config->filters = array_list_create(free); - char* dup = str_dup(argv[++i]); - if (!dup) - return -1; - array_list_add(config->filters, dup); - } else if (strcmp(argv[i], "--files-from") == 0 && i + 1 < argc) { - if (set_string_option(&config->files_from, argv[++i], "--files-from") != 0) - return -1; - } else if (strcmp(argv[i], "--cvs-exclude") == 0) { - config->cvs_exclude = true; - } else if (strcmp(argv[i], "--prune-empty-dirs") == 0) { - config->prune_empty_dirs = true; - } else if (strcmp(argv[i], "-R") == 0 || strcmp(argv[i], "--relative") == 0) { - config->relative = true; - } else if (strcmp(argv[i], "-e") == 0 || strcmp(argv[i], "--rsh") == 0) { - if (i + 1 < argc) { - if (set_string_option(&config->rsh_command, argv[++i], "-e/--rsh") != 0) - return -1; - } else { - fprintf(stderr, "Error: -e/--rsh requires a command argument\n"); - return -1; - } - } else if (strcmp(argv[i], "--rsync-path") == 0 && i + 1 < argc) { - if (set_string_option(&config->rsync_path, argv[++i], "--rsync-path") != 0) - return -1; - } else if (strcmp(argv[i], "--temp-dir") == 0 && i + 1 < argc) { - if (set_string_option(&config->temp_dir, argv[++i], "--temp-dir") != 0) - return -1; - } else if (strcmp(argv[i], "--compare-dest") == 0 && i + 1 < argc) { - if (set_string_option(&config->compare_dest, argv[++i], "--compare-dest") != 0) - return -1; - } else if (strcmp(argv[i], "--copy-dest") == 0 && i + 1 < argc) { - if (set_string_option(&config->copy_dest, argv[++i], "--copy-dest") != 0) - return -1; - } else if (strcmp(argv[i], "--link-dest") == 0 && i + 1 < argc) { - if (set_string_option(&config->link_dest, argv[++i], "--link-dest") != 0) - return -1; } else if (strcmp(argv[i], "--partial-dir") == 0 && i + 1 < argc) { if (set_string_option(&config->partial_dir, argv[++i], "--partial-dir") != 0) return -1; } else if (strcmp(argv[i], "--suffix") == 0 && i + 1 < argc) { if (set_string_option(&config->suffix, argv[++i], "--suffix") != 0) return -1; - } else if (strcmp(argv[i], "--delete-before") == 0) { - config->delete_before = true; } else if (strcmp(argv[i], "-T") == 0 && i + 1 < argc) { if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; - } else if (strcmp(argv[i], "--address") == 0 && i + 1 < argc) { - if (set_string_option(&config->address, argv[++i], "--address") != 0) - return -1; - } else if (strcmp(argv[i], "--bind-address") == 0 && i + 1 < argc) { - if (set_string_option(&config->bind_address, argv[++i], "--bind-address") != 0) - return -1; - } else if (strcmp(argv[i], "--ipv6") == 0) { - config->ipv6 = true; - } else if (strcmp(argv[i], "--ipv4") == 0) { - config->ipv4 = true; - } else if (strcmp(argv[i], "--daemon") == 0) { - config->daemon = true; - } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { - if (set_string_option(&config->daemon_config, argv[++i], "--config") != 0) - return -1; - } else if (strcmp(argv[i], "--server") == 0) { - config->server_mode = true; - } else if (strcmp(argv[i], "--checksum") == 0) { - config->checksum = true; - } else if (strcmp(argv[i], "--compress-choice") == 0 && i + 1 < argc) { - if (set_string_option(&config->compress_choice, argv[++i], "--compress-choice") != 0) - return -1; } else if (strcmp(argv[i], "--compress-level") == 0 && i + 1 < argc) { if (set_positive_int_option(&config->compression_level, argv[++i], "--compress-level") != 0) return -1; diff --git a/src/client/client_send.c b/src/client/client_send.c index 16e0e66..d442186 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -34,7 +34,7 @@ static int send_dry_run_manifest(Config* config) { config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->exclude_count, config->include_patterns, config->include_count, config->max_size, config->min_size, config->max_depth, config->follow_symlinks, config->copy_links, - config->safe_links, config->copy_unsafe_links, config->checksum); + config->safe_links, config->copy_unsafe_links); if (!scanner) return -1; Chunk* chunk; @@ -375,8 +375,7 @@ static int scan_directory_multithreaded(void* pipeline_context) { context->config->exclude_patterns, context->config->exclude_count, context->config->include_patterns, context->config->include_count, context->config->max_size, context->config->min_size, context->config->max_depth, 4, context->config->follow_symlinks, - context->config->copy_links, context->config->safe_links, context->config->copy_unsafe_links, - context->config->checksum); + context->config->copy_links, context->config->safe_links, context->config->copy_unsafe_links); Chunk* current_chunk; while ((current_chunk = parallel_scanner_next(scanner)) != NULL) { @@ -540,7 +539,7 @@ int send_files(Config* config) { config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->exclude_count, config->include_patterns, config->include_count, config->max_size, config->min_size, config->max_depth, config->follow_symlinks, config->copy_links, - config->safe_links, config->copy_unsafe_links, config->checksum); + config->safe_links, config->copy_unsafe_links); Chunk* current_chunk; unsigned long long total_bytes = 0; int total_files = 0; diff --git a/src/client/scanner.c b/src/client/scanner.c index 848f66d..70ef96e 100644 --- a/src/client/scanner.c +++ b/src/client/scanner.c @@ -44,7 +44,7 @@ DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_ int include_count, unsigned long long max_size, unsigned long long min_size, int max_depth, bool follow_symlinks, bool copy_links, bool safe_links, - bool copy_unsafe_links, bool checksum) { + bool copy_unsafe_links) { DirectoryScanner* scanner = malloc(sizeof(DirectoryScanner)); if (scanner == NULL) return NULL; @@ -65,7 +65,6 @@ DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_ scanner->copy_links = copy_links; scanner->safe_links = safe_links; scanner->copy_unsafe_links = copy_unsafe_links; - scanner->checksum = checksum; DirEntry* root = dir_entry_create(root_directory, 0); if (!root) { queue_destroy(scanner->directories); @@ -292,7 +291,6 @@ typedef struct { bool copy_links; bool safe_links; bool copy_unsafe_links; - bool checksum; } ParallelWorkerArg; static int parallel_worker_thread(void* arg) { @@ -301,7 +299,7 @@ static int parallel_worker_thread(void* arg) { DirectoryScanner* ds = directory_scanner_create( wa->dirs[i], wa->use_metadata, wa->chunk_size, wa->exclude_patterns, wa->exclude_count, wa->include_patterns, wa->include_count, wa->max_size, wa->min_size, wa->max_depth, - wa->follow_symlinks, wa->copy_links, wa->safe_links, wa->copy_unsafe_links, wa->checksum); + wa->follow_symlinks, wa->copy_links, wa->safe_links, wa->copy_unsafe_links); Chunk* chunk; while ((chunk = directory_scanner_next(ds)) != NULL) { queue_enqueue_multithreaded(wa->ps->result_queue, chunk, &wa->ps->result_mutex, @@ -329,7 +327,7 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata int include_count, unsigned long long max_size, unsigned long long min_size, int max_depth, int num_threads, bool follow_symlinks, bool copy_links, - bool safe_links, bool copy_unsafe_links, bool checksum) { + bool safe_links, bool copy_unsafe_links) { ParallelScanner* ps = calloc(1, sizeof(ParallelScanner)); if (!ps) return NULL; @@ -562,7 +560,6 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata wa->copy_links = copy_links; wa->safe_links = safe_links; wa->copy_unsafe_links = copy_unsafe_links; - wa->checksum = checksum; start += count; if (thrd_create(&ps->threads[t], parallel_worker_thread, wa) != thrd_success) { for (int j = 0; j < count; j++) diff --git a/src/client/scanner.h b/src/client/scanner.h index e4538e4..f3e0f0c 100644 --- a/src/client/scanner.h +++ b/src/client/scanner.h @@ -25,7 +25,6 @@ typedef struct { bool copy_links; bool safe_links; bool copy_unsafe_links; - bool checksum; } DirectoryScanner; typedef struct { @@ -46,7 +45,7 @@ DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_ int include_count, unsigned long long max_size, unsigned long long min_size, int max_depth, bool follow_symlinks, bool copy_links, bool safe_links, - bool copy_unsafe_links, bool checksum); + bool copy_unsafe_links); Chunk* directory_scanner_next(DirectoryScanner* scanner); void directory_scanner_destroy(DirectoryScanner* scanner); @@ -56,7 +55,7 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata int include_count, unsigned long long max_size, unsigned long long min_size, int max_depth, int num_threads, bool follow_symlinks, bool copy_links, - bool safe_links, bool copy_unsafe_links, bool checksum); + bool safe_links, bool copy_unsafe_links); Chunk* parallel_scanner_next(ParallelScanner* scanner); void parallel_scanner_destroy(ParallelScanner* scanner); diff --git a/src/client/usage.c b/src/client/usage.c index 2c477ea..918c293 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -51,19 +51,12 @@ static __attribute__((unused)) void print_usage() { printf(" --timeout I/O timeout in seconds (default: 30)\n"); printf(" -T Alias for --timeout\n"); printf(" --contimeout Connection timeout in seconds (default: 10)\n"); - printf(" --address Server hostname/IP to connect to\n"); - printf(" --bind-address Bind to specific local address\n"); - printf(" --ipv6 Prefer IPv6 connections\n"); - printf(" --ipv4 Prefer IPv4 connections\n"); - printf(" -q, --quiet Suppress non-error output\n"); - printf(" --silent Alias for --quiet\n"); printf(" --backup Backup existing files before overwriting\n"); printf(" --backup-dir Directory for backups (requires --backup)\n"); printf(" --suffix Backup suffix (default: ~)\n"); printf(" --stats Print transfer statistics at end\n"); printf(" --max-depth Maximum directory depth (0=unlimited)\n"); printf(" --log-file Write log messages to file\n"); - printf(" --queue-size Queue capacity for multithreaded mode (default: 100)\n"); printf(" --partial Keep partial files on interrupted transfer\n"); printf(" --partial-dir Directory for partial files\n"); printf(" --fastsync-server-path \n"); @@ -72,42 +65,9 @@ static __attribute__((unused)) void print_usage() { printf(" --copy-links Transform symlinks into referent files\n"); printf(" --safe-links Skip symlinks that point outside transfer tree\n"); printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n"); - printf(" -H, --hard-links Preserve hard links\n"); - printf(" -A, --acls Preserve ACLs\n"); - printf(" -X, --xattrs Preserve extended attributes\n"); - printf(" -D, --devices Preserve device files\n"); printf(" -S, --sparse Handle sparse files efficiently\n"); - printf(" -i, --itemize-changes Show per-file change summary\n"); - printf(" --out-format Custom output format string\n"); - printf(" --info Info verbosity level\n"); - printf(" --debug Debug verbosity level\n"); - printf(" --list-only List files without transferring\n"); - printf(" -h, --human-readable Human-readable numbers\n"); - printf(" -u, --update Skip files newer on destination\n"); printf(" --inplace Update files in-place (no temp+rename)\n"); - printf(" --append Append data to shorter files\n"); - printf(" --append-verify Append with verify\n"); - printf(" --delete-before Delete before transfer\n"); - printf(" --delete-excluded Also delete excluded files\n"); - printf(" --delete-after Delete after transfer, not before\n"); - printf(" --max-delete Maximum number of files to delete\n"); - printf(" --filter Add file filtering rule\n"); - printf(" --files-from Read file list from file\n"); - printf(" --cvs-exclude Auto-ignore CVS files\n"); - printf(" --prune-empty-dirs Omit empty directories from transfer\n"); - printf(" -R, --relative Use relative paths\n"); - printf(" -e, --rsh Specify remote shell\n"); - printf(" --rsync-path Path to remote binary\n"); - printf(" --daemon Run in daemon mode\n"); - printf(" --config Path to configuration file\n"); - printf(" --server Run in server mode\n"); - printf(" --checksum Skip files based on checksum, not mod-time/size\n"); - printf(" --compress-choice Compression algorithm (default: zstd)\n"); printf(" --compress-level Compression level (default: 5)\n"); - printf(" --temp-dir Temporary directory for files\n"); - printf(" --compare-dest Compare destination\n"); - printf(" --copy-dest Copy destination\n"); - printf(" --link-dest Link destination\n"); printf(" --help Show this help\n"); printf(" -V, --version Show version\n"); } diff --git a/src/shared/config.c b/src/shared/config.c index cc8b8e6..d09c655 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -45,59 +45,20 @@ static void config_set_defaults(Config* config) { config->server_port = 8080; config->timeout = 30; config->contimeout = 10; - config->quiet = false; config->backup = false; config->backup_dir = NULL; config->stats = false; config->max_depth = 0; config->log_file = NULL; - config->queue_size = 100; config->follow_symlinks = false; config->partial = false; config->copy_links = false; config->safe_links = false; config->copy_unsafe_links = false; - config->preserve_hard_links = false; - config->preserve_acls = false; - config->preserve_xattrs = false; - config->preserve_devices = false; config->preserve_sparse = false; - config->itemize_changes = false; - config->out_format = NULL; - config->info_level = 0; - config->debug_level = 0; - config->list_only = false; - config->human_readable = false; - config->update = false; config->inplace = false; - config->append = false; - config->append_verify = false; - config->delete_excluded = false; - config->delete_after = false; - config->max_delete = 0; - config->filters = NULL; - config->files_from = NULL; - config->cvs_exclude = false; - config->prune_empty_dirs = false; - config->relative = false; - config->rsh_command = NULL; - config->rsync_path = NULL; - config->temp_dir = NULL; - config->compare_dest = NULL; - config->copy_dest = NULL; - config->link_dest = NULL; config->partial_dir = NULL; config->suffix = NULL; - config->delete_before = false; - config->address = NULL; - config->bind_address = NULL; - config->ipv6 = false; - config->ipv4 = false; - config->daemon = false; - config->daemon_config = NULL; - config->server_mode = false; - config->checksum = false; - config->compress_choice = NULL; } Config* config_create(void) { @@ -153,23 +114,8 @@ void config_delete(Config* config) { free(config->tls_ca); free(config->backup_dir); free(config->server_host); - free(config->out_format); - free(config->files_from); - free(config->rsh_command); - free(config->rsync_path); - free(config->temp_dir); - free(config->compare_dest); - free(config->copy_dest); - free(config->link_dest); free(config->partial_dir); free(config->suffix); - free(config->address); - free(config->bind_address); - free(config->daemon_config); - free(config->compress_choice); - if (config->filters) { - array_list_delete(config->filters); - } free(config); } @@ -178,10 +124,11 @@ void config_delete(Config* config) { * use_chunk_serialization, use_compression, use_metadata, compression_level, chunk_size, * use_sendfile, use_delete, use_incremental, use_delta, delta_block_size, delta_max_file_size, * backup, backup_dir, follow_symlinks, copy_links, safe_links, copy_unsafe_links, - * preserve_hard_links, preserve_acls, preserve_xattrs, preserve_devices, preserve_sparse, - * update, inplace, append, append_verify, delete_excluded, delete_after, max_delete, relative, - * prune_empty_dirs, temp_dir, partial, partial_dir, suffix, delete_before, checksum, - * compress_choice, status + * obsolete metadata flags, preserve_sparse, obsolete transfer flags, inplace, obsolete delete + * flags, obsolete path options, partial, partial_dir, suffix, obsolete checksum options, status + * + * Obsolete fields remain as zero/empty compatibility slots. They must be consumed in this order + * until the protocol version is intentionally changed. */ bool config_send(int file_descriptor, const Config* config) { if (!send_str(file_descriptor, config->version)) @@ -228,35 +175,36 @@ bool config_send(int file_descriptor, const Config* config) { return false; if (!send_int(file_descriptor, config->copy_unsafe_links)) return false; - if (!send_int(file_descriptor, config->preserve_hard_links)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->preserve_acls)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->preserve_xattrs)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->preserve_devices)) + if (!send_int(file_descriptor, 0)) return false; if (!send_int(file_descriptor, config->preserve_sparse)) return false; - if (!send_int(file_descriptor, config->update)) + if (!send_int(file_descriptor, 0)) return false; if (!send_int(file_descriptor, config->inplace)) return false; - if (!send_int(file_descriptor, config->append)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->append_verify)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->delete_excluded)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->delete_after)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_n_data(file_descriptor, &config->max_delete, sizeof(config->max_delete))) + int obsolete_int = 0; + if (!send_n_data(file_descriptor, &obsolete_int, sizeof(obsolete_int))) return false; - if (!send_int(file_descriptor, config->relative)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->prune_empty_dirs)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_str(file_descriptor, config->temp_dir ? config->temp_dir : "")) + if (!send_str(file_descriptor, "")) return false; if (!send_int(file_descriptor, config->partial)) return false; @@ -264,11 +212,11 @@ bool config_send(int file_descriptor, const Config* config) { return false; if (!send_str(file_descriptor, config->suffix ? config->suffix : "")) return false; - if (!send_int(file_descriptor, config->delete_before)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_int(file_descriptor, config->checksum)) + if (!send_int(file_descriptor, 0)) return false; - if (!send_str(file_descriptor, config->compress_choice ? config->compress_choice : "")) + if (!send_str(file_descriptor, "")) return false; Status status; if (!receive_status(file_descriptor, &status)) @@ -374,48 +322,39 @@ Config* config_receive(int file_descriptor) { config->copy_unsafe_links = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->preserve_hard_links = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->preserve_acls = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->preserve_xattrs = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->preserve_devices = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; config->preserve_sparse = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->update = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; config->inplace = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->append = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->append_verify = tmp; - if (!receive_int(file_descriptor, &tmp)) - goto error; - config->delete_excluded = tmp; - if (!receive_int(file_descriptor, &tmp)) - goto error; - config->delete_after = tmp; - if (!receive_n_data(file_descriptor, &config->max_delete, sizeof(config->max_delete))) - goto error; if (!receive_int(file_descriptor, &tmp)) goto error; - config->relative = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->prune_empty_dirs = tmp; - config->temp_dir = receive_str(file_descriptor); - if (config->temp_dir == NULL) + int obsolete_int = 0; + if (!receive_n_data(file_descriptor, &obsolete_int, sizeof(obsolete_int))) goto error; + if (!receive_int(file_descriptor, &tmp)) + goto error; + if (!receive_int(file_descriptor, &tmp)) + goto error; + char* obsolete_string = receive_str(file_descriptor); + if (obsolete_string == NULL) + goto error; + free(obsolete_string); if (!receive_int(file_descriptor, &tmp)) goto error; config->partial = tmp; @@ -427,13 +366,12 @@ Config* config_receive(int file_descriptor) { goto error; if (!receive_int(file_descriptor, &tmp)) goto error; - config->delete_before = tmp; if (!receive_int(file_descriptor, &tmp)) goto error; - config->checksum = tmp; - config->compress_choice = receive_str(file_descriptor); - if (config->compress_choice == NULL) + obsolete_string = receive_str(file_descriptor); + if (obsolete_string == NULL) goto error; + free(obsolete_string); if (!send_status(file_descriptor, STATUS_OK)) goto error; return config; @@ -444,10 +382,8 @@ error: free(config->receive_root_directory); free(config->server_host); free(config->backup_dir); - free(config->temp_dir); free(config->partial_dir); free(config->suffix); - free(config->compress_choice); free(config); return NULL; } diff --git a/src/shared/config.h b/src/shared/config.h index 22359e4..f0153ec 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -1,7 +1,6 @@ #ifndef CONFIG_H #define CONFIG_H -#include "array_list.h" #include #include #include @@ -45,13 +44,11 @@ typedef struct Config { char* tls_ca; int timeout; int contimeout; - bool quiet; bool backup; char* backup_dir; bool stats; int max_depth; FILE* log_file; - int queue_size; bool follow_symlinks; bool partial; @@ -60,46 +57,9 @@ typedef struct Config { bool safe_links; bool copy_unsafe_links; - // Issue #121: Extended metadata preservation - bool preserve_hard_links; - bool preserve_acls; - bool preserve_xattrs; - bool preserve_devices; bool preserve_sparse; - // Issue #122: Output/logging options - bool itemize_changes; - char* out_format; - int info_level; - int debug_level; - bool list_only; - bool human_readable; - - // Issue #127: Transfer modes - bool update; bool inplace; - bool append; - bool append_verify; - - // Issue #128: Extended delete options - bool delete_excluded; - bool delete_after; - int max_delete; - - // Issue #129: Advanced file selection - ArrayList* filters; - char* files_from; - bool cvs_exclude; - bool prune_empty_dirs; - bool relative; - - // Issue #130: Remote shell/connection options - char* rsh_command; - char* rsync_path; - char* temp_dir; - char* compare_dest; - char* copy_dest; - char* link_dest; // PR #174: Partial transfer resumption char* partial_dir; @@ -107,25 +67,6 @@ typedef struct Config { // PR #178: Backup versioning char* suffix; - // PR #179: Delete policies - bool delete_before; - - // PR #181: IPv6 and bind address - char* address; - char* bind_address; - bool ipv6; - bool ipv4; - - // PR #182: Daemon/server mode - bool daemon; - char* daemon_config; - bool server_mode; - - // PR #183: Checksum comparison - bool checksum; - - // PR #184: Compression algorithm negotiation - char* compress_choice; } Config; /* This version must be bumped whenever config_send / config_receive wire format changes. */ diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index cf13dfe..431ceed 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -207,6 +207,34 @@ static void test_parse_args_unknown_option() { config_delete(cfg); } +/* Parsed-but-unimplemented options must fail instead of being silently accepted. */ +static void test_parse_args_rejects_unimplemented_options() { + static const char* const options[] = { + "-q", "--quiet", "--silent", "--queue-size", + "-H", "--hard-links", "-A", "--acls", + "-X", "--xattrs", "-D", "--devices", + "-i", "--itemize-changes", "--out-format", "--info", + "--debug", "--list-only", "-h", "--human-readable", + "-u", "--update", "--append", "--append-verify", + "--delete-excluded", "--delete-after", "--max-delete", "--filter", + "--files-from", "--cvs-exclude", "--prune-empty-dirs", "-R", + "--relative", "-e", "--rsh", "--rsync-path", + "--temp-dir", "--compare-dest", "--copy-dest", "--link-dest", + "--delete-before", "--address", "--bind-address", "--ipv6", + "--ipv4", "--daemon", "--config", "--server", + "--checksum", "--compress-choice"}; + + for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { + Config* cfg = config_create(); + char* argv[] = {"fastsync", (char*)options[i], "dummy", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1); + config_delete(cfg); + } +} + /* Test parse_args with --archive flag */ static void test_parse_args_archive() { Config* cfg = config_create(); @@ -238,5 +266,6 @@ void test_client_cli() { test_parse_args_invalid_compression_level(); test_parse_args_valid_compression_level(); test_parse_args_unknown_option(); + test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); } diff --git a/tests/test_scanner.c b/tests/test_scanner.c index 41609f2..b49e6fb 100644 --- a/tests/test_scanner.c +++ b/tests/test_scanner.c @@ -19,7 +19,7 @@ static void test_scanner_single_file() { create_test_file(file1, content1); DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, - 0, false, false, false, false, false); + 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -48,7 +48,7 @@ static void test_scanner_multiple_files() { create_test_file(file2, content2); DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, - 0, false, false, false, false, false); + 0, false, false, false, false); EXPECT_NOT_NULL(scanner); const Chunk* chunk = directory_scanner_next(scanner); @@ -88,7 +88,7 @@ static void test_scanner_subdirectory() { create_test_file(sub_file, content); DirectoryScanner* scanner = directory_scanner_create((char*)root, false, 0, NULL, 0, NULL, 0, 0, - 0, 0, false, false, false, false, false); + 0, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); int total_files = 0; @@ -112,7 +112,7 @@ static void test_scanner_empty_directory() { EXPECT_EQ_INT(mkdir(dir, 0755), 0); DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, - 0, false, false, false, false, false); + 0, false, false, false, false); EXPECT_NOT_NULL(scanner); const Chunk* chunk = directory_scanner_next(scanner); @@ -136,7 +136,7 @@ static void test_scanner_exclude_pattern() { char* exclude[] = {"*.tmp"}; DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, exclude, 1, NULL, 0, 0, - 0, 0, false, false, false, false, false); + 0, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -169,7 +169,7 @@ static void test_scanner_exclude_subdirectory() { char* exclude[] = {"*.tmp"}; DirectoryScanner* scanner = directory_scanner_create((char*)root, false, 0, exclude, 1, NULL, 0, - 0, 0, 0, false, false, false, false, false); + 0, 0, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); int total = 0; @@ -207,7 +207,7 @@ static void test_scanner_include_and_exclude() { char* exclude[] = {"*.bak"}; char* include[] = {"*.txt", "*.log"}; DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, exclude, 1, include, 2, - 0, 0, 0, false, false, false, false, false); + 0, 0, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -244,7 +244,7 @@ static void test_scanner_max_size() { /* max_size = 10 — only files <= 10 bytes */ DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 10, - 0, 0, false, false, false, false, false); + 0, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -272,7 +272,7 @@ static void test_scanner_min_size() { /* min_size = 1 — only files >= 1 byte */ DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 1, - 0, false, false, false, false, false); + 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -302,7 +302,7 @@ static void test_scanner_size_range() { /* Only files between 3 and 20 bytes */ DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 20, - 3, 0, false, false, false, false, false); + 3, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -338,7 +338,7 @@ static void test_scanner_mixed_patterns() { char* exclude[] = {"*.bak"}; char* include[] = {"*.txt"}; DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, exclude, 1, include, 1, - 10, 3, 0, false, false, false, false, false); + 10, 3, 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); @@ -369,7 +369,7 @@ static void test_scanner_no_patterns() { create_test_file(f2, "second"); DirectoryScanner* scanner = directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, - 0, false, false, false, false, false); + 0, false, false, false, false); EXPECT_NOT_NULL(scanner); Chunk* chunk = directory_scanner_next(scanner); From 2e00fc31f2ff2b2b8eb978cae2653aea7fee2ef7 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 11 Aug 2026 21:08:54 +0200 Subject: [PATCH 3/5] Format CLI option test table --- tests/test_client_cli.c | 64 ++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 431ceed..b0a51bb 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -209,20 +209,56 @@ static void test_parse_args_unknown_option() { /* Parsed-but-unimplemented options must fail instead of being silently accepted. */ static void test_parse_args_rejects_unimplemented_options() { - static const char* const options[] = { - "-q", "--quiet", "--silent", "--queue-size", - "-H", "--hard-links", "-A", "--acls", - "-X", "--xattrs", "-D", "--devices", - "-i", "--itemize-changes", "--out-format", "--info", - "--debug", "--list-only", "-h", "--human-readable", - "-u", "--update", "--append", "--append-verify", - "--delete-excluded", "--delete-after", "--max-delete", "--filter", - "--files-from", "--cvs-exclude", "--prune-empty-dirs", "-R", - "--relative", "-e", "--rsh", "--rsync-path", - "--temp-dir", "--compare-dest", "--copy-dest", "--link-dest", - "--delete-before", "--address", "--bind-address", "--ipv6", - "--ipv4", "--daemon", "--config", "--server", - "--checksum", "--compress-choice"}; + static const char* const options[] = {"-q", + "--quiet", + "--silent", + "--queue-size", + "-H", + "--hard-links", + "-A", + "--acls", + "-X", + "--xattrs", + "-D", + "--devices", + "-i", + "--itemize-changes", + "--out-format", + "--info", + "--debug", + "--list-only", + "-h", + "--human-readable", + "-u", + "--update", + "--append", + "--append-verify", + "--delete-excluded", + "--delete-after", + "--max-delete", + "--filter", + "--files-from", + "--cvs-exclude", + "--prune-empty-dirs", + "-R", + "--relative", + "-e", + "--rsh", + "--rsync-path", + "--temp-dir", + "--compare-dest", + "--copy-dest", + "--link-dest", + "--delete-before", + "--address", + "--bind-address", + "--ipv6", + "--ipv4", + "--daemon", + "--config", + "--server", + "--checksum", + "--compress-choice"}; for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { Config* cfg = config_create(); From 679e389ba66bc89095a9ccfae2154e74e11381e3 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 11 Aug 2026 21:12:00 +0200 Subject: [PATCH 4/5] Fix va_list cleanup in logger --- src/shared/log.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/log.c b/src/shared/log.c index c22a853..37a1a73 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -42,8 +42,11 @@ void log_message(LogLevel log_level, const char* format, ...) { va_list args; va_start(args, format); write_message(dest_io, log_level, t, format, args); + va_end(args); if (log_fp) { + va_start(args, format); write_message(log_fp, log_level, t, format, args); + va_end(args); } } From 852e47a143e31e1a974a4d10017ba32626e7d6f8 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 11 Aug 2026 21:14:56 +0200 Subject: [PATCH 5/5] Satisfy cppcheck CLI and logger checks --- src/client/client_cli.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 641d437..b0ef08b 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -55,7 +55,7 @@ static bool parse_positive_int(const char* s, int* out_val) { /* Duplicate a string argument into *dest, freeing the old value. Returns true on success, false on * failure. */ -static bool set_string_option(char** dest, const char* value, const char* option_name) { +static int set_string_option(char** dest, const char* value, const char* option_name) { char* dup = str_dup(value); if (!dup) { fprintf(stderr, "Error: memory allocation failed for %s\n", option_name); @@ -67,7 +67,7 @@ static bool set_string_option(char** dest, const char* value, const char* option } /* Parse a string as a positive integer into *dest. Returns true on success, false on error. */ -static bool set_positive_int_option(int* dest, const char* value, const char* option_name) { +static int set_positive_int_option(int* dest, const char* value, const char* option_name) { if (!parse_positive_int(value, dest)) { fprintf(stderr, "Error: %s must be a positive integer\n", option_name); return -1; @@ -76,7 +76,7 @@ static bool set_positive_int_option(int* dest, const char* value, const char* op } /* Parse a string as a non-negative integer into *dest. Returns true on success, false on error. */ -static bool set_nonneg_int_option(int* dest, const char* value, const char* option_name) { +static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) { if (!parse_nonneg_int(value, dest)) { fprintf(stderr, "Error: %s must be a non-negative integer\n", option_name); return -1;