From 60eb7f11fee154420594c07f03d1a1bb1e2c2943 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 16:07:51 +0200 Subject: [PATCH 1/2] feat: add rsync stderr output modes --- RSYNC_COMPAT.md | 2 +- src/client/client_cli.c | 20 ++++++++++++++++++++ src/client/usage.c | 1 + src/shared/log.c | 11 ++++++++++- src/shared/log.h | 3 +++ tests/test_client_cli.c | 31 +++++++++++++++++++++++++++++++ tests/test_log.c | 26 ++++++++++++++++++++++++++ 7 files changed, 92 insertions(+), 2 deletions(-) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..cc75281 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -25,7 +25,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `-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 | | +| `--stderr=MODE` | Change stderr output mode | ⚠️ Partial | `errors` (default), `all`, and `client` are accepted; `client` preserves local client routing because FastSync has no rsync message channel | | `--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 | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..e85b545 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -86,6 +86,20 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio return 0; } +static int set_stderr_mode(const char* value) { + if (strcmp(value, "errors") == 0 || strcmp(value, "e") == 0) + log_set_stderr_mode(LOG_STDERR_ERRORS); + else if (strcmp(value, "all") == 0 || strcmp(value, "a") == 0) + log_set_stderr_mode(LOG_STDERR_ALL); + else if (strcmp(value, "client") == 0 || strcmp(value, "c") == 0) + log_set_stderr_mode(LOG_STDERR_CLIENT); + else { + log_message(LOG_LEVEL_ERROR, "--stderr must be errors, all, or client"); + return -1; + } + return 0; +} + static int read_patterns_from_file(const char* filepath, char*** patterns, int* count); /* Parse a string as an unsigned long long. Returns 0 on success, -1 on error. */ @@ -341,6 +355,12 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } config->log_file = lf; log_set_file(lf); + } else if (strncmp(argv[i], "--stderr=", 9) == 0) { + if (set_stderr_mode(argv[i] + 9) != 0) + return -1; + } else if (opt_is(argv[i], "--stderr", NULL)) { + if (i + 1 >= argc || set_stderr_mode(argv[++i]) != 0) + return -1; } else if (opt_is(argv[i], "--exclude-from", NULL) && i + 1 < argc) { if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != 0) diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..69fd5cf 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -58,6 +58,7 @@ void print_usage(void) { 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(" --stderr=MODE Route logging to stderr: errors, all, or client\n"); printf(" --partial Keep partial files on interrupted transfer\n"); printf(" --partial-dir Directory for partial files\n"); printf(" --fastsync-server-path \n"); diff --git a/src/shared/log.c b/src/shared/log.c index 29bbe0d..c345f49 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -8,6 +8,7 @@ static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static LogLevel current_log_level = LOG_LEVEL_WARNING; static FILE* log_fp = NULL; +static LogStderrMode stderr_mode = LOG_STDERR_ERRORS; void set_log_level(LogLevel level) { current_log_level = level; @@ -17,6 +18,14 @@ void log_set_file(FILE* fp) { log_fp = fp; } +void log_set_stderr_mode(LogStderrMode mode) { + stderr_mode = mode; +} + +LogStderrMode log_get_stderr_mode(void) { + return stderr_mode; +} + 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, @@ -37,7 +46,7 @@ void log_message(LogLevel log_level, const char* format, ...) { return; FILE* dest_io = stdout; - if (log_level == LOG_LEVEL_ERROR) { + if (stderr_mode == LOG_STDERR_ALL || log_level == LOG_LEVEL_ERROR) { dest_io = stderr; } diff --git a/src/shared/log.h b/src/shared/log.h index acea629..2fe216d 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -4,10 +4,13 @@ #include typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; +typedef enum { LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT } LogStderrMode; void log_message(LogLevel log_level, const char* message, ...); void log_perror(const char* context); void set_log_level(LogLevel level); void log_set_file(FILE* fp); +void log_set_stderr_mode(LogStderrMode mode); +LogStderrMode log_get_stderr_mode(void); #endif diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..e809112 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -1,6 +1,7 @@ #include "test_client_cli.h" #include "client_validation.h" #include "config.h" +#include "log.h" #include "test_utils.h" #include "utils.h" #include @@ -339,6 +340,34 @@ static void test_parse_args_archive() { config_delete(cfg); } +static void test_parse_args_stderr_modes() { + static const char* const modes[] = {"errors", "all", "client", "e", "a", "c"}; + static const LogStderrMode expected[] = {LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT, + LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT}; + for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { + Config* cfg = config_create(); + char option[32]; + snprintf(option, sizeof(option), "--stderr=%s", modes[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), 0); + EXPECT_EQ_INT(log_get_stderr_mode(), expected[i]); + config_delete(cfg); + } + log_set_stderr_mode(LOG_STDERR_ERRORS); +} + +static void test_parse_args_rejects_invalid_stderr_mode() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--stderr=invalid", "/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); + log_set_stderr_mode(LOG_STDERR_ERRORS); +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -360,4 +389,6 @@ void test_client_cli() { test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); + test_parse_args_stderr_modes(); + test_parse_args_rejects_invalid_stderr_mode(); } diff --git a/tests/test_log.c b/tests/test_log.c index da828dc..61b67a3 100644 --- a/tests/test_log.c +++ b/tests/test_log.c @@ -1,6 +1,8 @@ #include "test_log.h" #include "log.h" #include "test_utils.h" +#include +#include /* Test default log level: WARNING and ERROR should print, DEBUG and INFO should not. * We can't easily capture stderr in unit tests, so we verify the functions don't crash @@ -90,6 +92,29 @@ static void test_log_filtering() { EXPECT_TRUE(true); } +static void test_log_stderr_mode_all() { + int pipe_fds[2]; + EXPECT_EQ_INT(pipe(pipe_fds), 0); + int saved_stderr = dup(STDERR_FILENO); + EXPECT_TRUE(saved_stderr >= 0); + EXPECT_TRUE(dup2(pipe_fds[1], STDERR_FILENO) >= 0); + close(pipe_fds[1]); + + set_log_level(LOG_LEVEL_WARNING); + log_set_stderr_mode(LOG_STDERR_ALL); + log_message(LOG_LEVEL_WARNING, "warning routed to stderr"); + fflush(stderr); + + EXPECT_TRUE(dup2(saved_stderr, STDERR_FILENO) >= 0); + close(saved_stderr); + char output[128] = {0}; + ssize_t length = read(pipe_fds[0], output, sizeof(output) - 1); + close(pipe_fds[0]); + EXPECT_TRUE(length > 0); + EXPECT_TRUE(strstr(output, "warning routed to stderr") != NULL); + log_set_stderr_mode(LOG_STDERR_ERRORS); +} + /* Test that log_message handles various format strings */ static void test_log_message_formats() { set_log_level(LOG_LEVEL_DEBUG); @@ -112,5 +137,6 @@ void test_log() { test_log_set_level_info(); test_log_set_level_error(); test_log_filtering(); + test_log_stderr_mode_all(); test_log_message_formats(); } From 5350290f528583bed6c3a7bd9de12a61d4e39f1e Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 21:21:22 +0200 Subject: [PATCH 2/2] fix: reject unsupported rsync client stderr mode --- RSYNC_COMPAT.md | 12 ++++++------ src/client/client_cli.c | 10 ++++++---- src/client/usage.c | 2 +- src/shared/log.h | 2 +- tests/test_client_cli.c | 27 ++++++++++++++++----------- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index cc75281..f12f2f6 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -6,11 +6,11 @@ This document maps rsync's full feature set to FastSync's current implementation | Status | Count | Description | |--------|-------|-------------| -| ✅ 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** | | +| ✅ Implemented | 35 | Feature works end-to-end | +| 🔀 Alt Arg | 4 | Functionality exists but under different flag/semantics | +| ⚠️ Partial | 5 | Flag parsed/stored but behavior incomplete | +| ❌ Not Implemented | 97 | Flag not recognized or no behavior | +| **Total** | **141** | | --- @@ -25,7 +25,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `-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 | ⚠️ Partial | `errors` (default), `all`, and `client` are accepted; `client` preserves local client routing because FastSync has no rsync message channel | +| `--stderr=MODE` | Change stderr output mode | ⚠️ Partial | `errors` (default) and `all` are supported; `client` is rejected because FastSync has no rsync message channel | | `--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 | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index e85b545..dce9c79 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -91,10 +91,12 @@ static int set_stderr_mode(const char* value) { log_set_stderr_mode(LOG_STDERR_ERRORS); else if (strcmp(value, "all") == 0 || strcmp(value, "a") == 0) log_set_stderr_mode(LOG_STDERR_ALL); - else if (strcmp(value, "client") == 0 || strcmp(value, "c") == 0) - log_set_stderr_mode(LOG_STDERR_CLIENT); - else { - log_message(LOG_LEVEL_ERROR, "--stderr must be errors, all, or client"); + else if (strcmp(value, "client") == 0 || strcmp(value, "c") == 0) { + log_message(LOG_LEVEL_ERROR, + "--stderr=client is not supported: FastSync has no client message channel"); + return -1; + } else { + log_message(LOG_LEVEL_ERROR, "--stderr must be errors or all"); return -1; } return 0; diff --git a/src/client/usage.c b/src/client/usage.c index 69fd5cf..5c9c5f1 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -58,7 +58,7 @@ void print_usage(void) { 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(" --stderr=MODE Route logging to stderr: errors, all, or client\n"); + printf(" --stderr=MODE Route logging to stderr: errors or all\n"); printf(" --partial Keep partial files on interrupted transfer\n"); printf(" --partial-dir Directory for partial files\n"); printf(" --fastsync-server-path \n"); diff --git a/src/shared/log.h b/src/shared/log.h index 2fe216d..24237c8 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -4,7 +4,7 @@ #include typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; -typedef enum { LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT } LogStderrMode; +typedef enum { LOG_STDERR_ERRORS, LOG_STDERR_ALL } LogStderrMode; void log_message(LogLevel log_level, const char* message, ...); void log_perror(const char* context); diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index e809112..6e53f2f 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -341,9 +341,9 @@ static void test_parse_args_archive() { } static void test_parse_args_stderr_modes() { - static const char* const modes[] = {"errors", "all", "client", "e", "a", "c"}; - static const LogStderrMode expected[] = {LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT, - LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT}; + static const char* const modes[] = {"errors", "all", "e", "a"}; + static const LogStderrMode expected[] = {LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_ERRORS, + LOG_STDERR_ALL}; for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { Config* cfg = config_create(); char option[32]; @@ -358,13 +358,18 @@ static void test_parse_args_stderr_modes() { log_set_stderr_mode(LOG_STDERR_ERRORS); } -static void test_parse_args_rejects_invalid_stderr_mode() { - Config* cfg = config_create(); - char* argv[] = {"fastsync", "--stderr=invalid", "/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_unsupported_stderr_modes() { + static const char* const modes[] = {"client", "c", "invalid"}; + for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { + Config* cfg = config_create(); + char option[32]; + snprintf(option, sizeof(option), "--stderr=%s", modes[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); + } log_set_stderr_mode(LOG_STDERR_ERRORS); } @@ -390,5 +395,5 @@ void test_client_cli() { test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); test_parse_args_stderr_modes(); - test_parse_args_rejects_invalid_stderr_mode(); + test_parse_args_rejects_unsupported_stderr_modes(); }