diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index d91b286..8f4960c 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -7,10 +7,10 @@ This document maps rsync's full feature set to FastSync's current implementation | Status | Count | Description | |--------|-------|-------------| | ✅ Implemented | 35 | 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** | **137** | | +| 🔀 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 | ❌ Not Implemented | | +| `--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 | @@ -248,12 +248,11 @@ Ranked by user demand, implementation complexity, and interoperability impact: | 2 | `--ignore-times` / `-I` | Low | Medium — useful for forcing re-transfer | | 3 | `--size-only` | Low | Medium — common migration scenario | | 4 | `--existing` / `--ignore-existing` | Low | Medium — common sync patterns | -| 5 | `--remove-source-files` | Low | High — common for moves/backup | -| 6 | `--delete-during` | Medium | High — performance improvement | -| 7 | `--delay-updates` | Medium | High — atomic updates | -| 8 | `--chmod` | Low | Medium — permission flexibility | -| 9 | `--executability` / `-E` | Low | Low — simple flag | -| 10 | `--skip-compress` | Low | Medium — performance tuning | +| 5 | `--delete-during` | Medium | High — performance improvement | +| 6 | `--delay-updates` | Medium | High — atomic updates | +| 7 | `--chmod` | Low | Medium — permission flexibility | +| 8 | `--executability` / `-E` | Low | Low — simple flag | +| 9 | `--skip-compress` | Low | Medium — performance tuning | --- diff --git a/src/client/client_cli.c b/src/client/client_cli.c index ec42b1e..a4a2a57 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -86,6 +86,22 @@ 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_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; +} + 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. */ @@ -355,6 +371,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 b8d2e70..a4c9034 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -62,6 +62,7 @@ void print_usage(void) { printf(" -h, --human-readable Print byte sizes in human-readable form\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 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.c b/src/shared/log.c index 78ef699..83db013 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -9,6 +9,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 _Thread_local bool eight_bit_output; +static LogStderrMode stderr_mode = LOG_STDERR_ERRORS; void set_log_level(LogLevel level) { current_log_level = level; @@ -26,6 +27,14 @@ bool log_get_8_bit_output(void) { return eight_bit_output; } +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, @@ -46,7 +55,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 704659b..0aa2943 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -5,6 +5,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 } LogStderrMode; void log_message(LogLevel log_level, const char* message, ...); void log_perror(const char* context); @@ -12,5 +13,7 @@ void set_log_level(LogLevel level); void log_set_file(FILE* fp); void log_set_8_bit_output(bool enabled); bool log_get_8_bit_output(void); +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 10fdb5a..a433e60 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 @@ -415,6 +416,39 @@ static void test_parse_args_8_bit_output() { config_delete(cfg); } +static void test_parse_args_stderr_modes() { + 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]; + 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_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); +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -442,4 +476,6 @@ void test_client_cli() { test_parse_args_archive(); test_parse_args_fsync(); test_parse_args_8_bit_output(); + test_parse_args_stderr_modes(); + test_parse_args_rejects_unsupported_stderr_modes(); } 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(); }