diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..71e3e1f 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -43,7 +43,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `--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 | | +| `--8-bit-output`, `-8` | Leave high-bit chars unescaped | ✅ Implemented | Applies to displayed paths and protocol debug output | | `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect | ## 3. File Selection diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..266ec64 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -153,6 +153,7 @@ static const OptionEntry OPTION_TABLE[] = { {"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)}, {"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)}, {"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)}, + {"--8-bit-output", "-8", OPT_FLAG, offsetof(Config, eight_bit_output)}, {"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)}, {"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)}, @@ -210,6 +211,7 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch /* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */ int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count) { + log_set_8_bit_output(config->eight_bit_output); for (int i = 1; i < argc; i++) { const OptionEntry* entry = find_table_option(argv[i]); if (entry) { @@ -223,6 +225,8 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (apply_table_option(config, entry, NULL) != 0) { return -1; } + if (entry->offset == offsetof(Config, eight_bit_output)) + log_set_8_bit_output(true); continue; } diff --git a/src/client/client_send.c b/src/client/client_send.c index 0b4ca5c..111e2c1 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -137,7 +137,14 @@ static int send_dry_run_manifest(const Config* config) { printf("Dry run: files to be transferred\n"); while ((chunk = directory_scanner_next(scanner)) != NULL) { for (int i = 0; i < chunk->element_count; i++) { - printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); + char* escaped_path = output_escape(chunk->items[i]->path, config->eight_bit_output); + if (!escaped_path) { + chunk_destroy(chunk); + directory_scanner_destroy(scanner); + return -1; + } + printf(" %s (%zu bytes)\n", escaped_path, chunk->items[i]->data->size); + free(escaped_path); total_bytes += chunk->items[i]->data->size; file_count++; } diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..4d495cd 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -20,6 +20,7 @@ void print_usage(void) { printf(" -n, --dry-run Show what would be transferred\n"); printf(" -p SSH port (default: 22)\n"); printf(" --progress Show transfer progress\n"); + printf(" -8, --8-bit-output Leave high-bit characters unescaped in output\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"); diff --git a/src/shared/config.c b/src/shared/config.c index efc2708..6468503 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -68,6 +68,7 @@ static void config_set_defaults(Config* config) { config->debug_level = 0; config->list_only = false; config->human_readable = false; + config->eight_bit_output = false; config->update = false; config->inplace = false; config->append = false; diff --git a/src/shared/config.h b/src/shared/config.h index 4a218b0..92df6ea 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -74,6 +74,7 @@ typedef struct Config { int debug_level; bool list_only; bool human_readable; + bool eight_bit_output; // Issue #127: Transfer modes bool update; diff --git a/src/shared/log.c b/src/shared/log.c index 29bbe0d..bf11296 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 bool eight_bit_output = false; 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_8_bit_output(bool enabled) { + eight_bit_output = enabled; +} + +bool log_get_8_bit_output(void) { + return eight_bit_output; +} + 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, diff --git a/src/shared/log.h b/src/shared/log.h index acea629..704659b 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -2,6 +2,7 @@ #define LOG_H #include +#include typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; @@ -9,5 +10,7 @@ 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_8_bit_output(bool enabled); +bool log_get_8_bit_output(void); #endif diff --git a/src/shared/protocol.c b/src/shared/protocol.c index d3e4aa3..728578a 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -1,5 +1,6 @@ #include "protocol.h" #include "log.h" +#include "utils.h" #include #include #include @@ -328,7 +329,9 @@ bool protocol_send_str(ProtocolSession* session, const char* data) { return false; if (!protocol_send_n_data(session, data, size)) return false; - log_message(LOG_LEVEL_DEBUG, "Send String: %s", data); + char* escaped = output_escape(data, log_get_8_bit_output()); + log_message(LOG_LEVEL_DEBUG, "Send String: %s", escaped ? escaped : ""); + free(escaped); return true; } @@ -356,7 +359,9 @@ char* protocol_receive_str(ProtocolSession* session) { } data[size] = '\0'; session->total_allocated_bytes += size + 1; - log_message(LOG_LEVEL_DEBUG, "Received String: %s", data); + char* escaped = output_escape(data, log_get_8_bit_output()); + log_message(LOG_LEVEL_DEBUG, "Received String: %s", escaped ? escaped : ""); + free(escaped); return data; } diff --git a/src/shared/utils.c b/src/shared/utils.c index 49c1367..3fe5851 100644 --- a/src/shared/utils.c +++ b/src/shared/utils.c @@ -1,6 +1,7 @@ #include "utils.h" #include "array_list.h" #include "libgen.h" +#include "log.h" #include #include #include @@ -132,6 +133,32 @@ char* str_dup(const char* string) { return new_string; } +char* output_escape(const char* string, bool eight_bit_output) { + if (!string) + return NULL; + size_t length = strlen(string); + if (length > (SIZE_MAX - 1) / 5) + return NULL; + char* escaped = malloc(length * 5 + 1); + if (!escaped) + return NULL; + size_t out = 0; + for (size_t i = 0; i < length; i++) { + unsigned char byte = (unsigned char)string[i]; + if ((byte >= 32 && byte <= 126) || (eight_bit_output && byte >= 128)) { + escaped[out++] = (char)byte; + } else { + escaped[out++] = '\\'; + escaped[out++] = '#'; + escaped[out++] = (char)('0' + ((byte >> 6) & 7)); + escaped[out++] = (char)('0' + ((byte >> 3) & 7)); + escaped[out++] = (char)('0' + (byte & 7)); + } + } + escaped[out] = '\0'; + return escaped; +} + /* Match a glob pattern against a string. Supported wildcards: * ? matches any single character except '/'. * * matches any sequence of characters within one path component (no '/'). @@ -274,7 +301,9 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes } else { (*deleted_count)++; } - fprintf(stderr, " Deleted: %s\n", child_rel); + char* escaped_path = output_escape(child_rel, log_get_8_bit_output()); + fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : ""); + free(escaped_path); } } free(child_rel); diff --git a/src/shared/utils.h b/src/shared/utils.h index 757d26f..aabea79 100644 --- a/src/shared/utils.h +++ b/src/shared/utils.h @@ -7,6 +7,7 @@ bool mkdir_r(const char* path); char* str_dup(const char* string); +char* output_escape(const char* string, bool eight_bit_output); char* path_cat(const char* path1, const char* path2); bool glob_match(const char* pattern, const char* str); bool delete_extras(const char* dest_root, ArrayList* manifest); diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..55b99dd 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -281,7 +281,6 @@ static void test_parse_args_rejects_unimplemented_options() { "--debug", "--list-only", "-h", - "--human-readable", "-u", "--update", "--append", diff --git a/tests/test_shared_utils.c b/tests/test_shared_utils.c index 57b1c74..79813d8 100644 --- a/tests/test_shared_utils.c +++ b/tests/test_shared_utils.c @@ -5,6 +5,14 @@ #include void test_shared_utils() { + char high_bit[] = {'a', (char)0xc3, (char)0xa9, '\n', '\0'}; + char* escaped = output_escape(high_bit, false); + EXPECT_EQ_STR(escaped, "a\\#303\\#251\\#012"); + free(escaped); + escaped = output_escape(high_bit, true); + EXPECT_EQ_STR(escaped, "a\xc3\xa9\\#012"); + free(escaped); + // Test str_dup const char* dup_null = str_dup(NULL); EXPECT_NULL(dup_null);