From eb4e9fba1f47de5ef4424bed7053af5c4dfdf0c7 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 16:03:19 +0200 Subject: [PATCH 1/3] feat: add 8-bit output option --- RSYNC_COMPAT.md | 2 +- src/client/client_cli.c | 4 ++++ src/client/client_send.c | 9 ++++++++- src/client/usage.c | 1 + src/shared/config.c | 1 + src/shared/config.h | 1 + src/shared/log.c | 9 +++++++++ src/shared/log.h | 3 +++ src/shared/protocol.c | 9 +++++++-- src/shared/utils.c | 31 ++++++++++++++++++++++++++++++- src/shared/utils.h | 1 + tests/test_client_cli.c | 1 - tests/test_shared_utils.c | 8 ++++++++ 13 files changed, 74 insertions(+), 6 deletions(-) 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); From 5b997d5d25f86c3743b8e5b9a74c862b9a646304 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 21:18:35 +0200 Subject: [PATCH 2/3] fix: complete 8-bit output negotiation --- README.md | 2 +- src/server/server.c | 1 + src/shared/config.c | 14 +++++++++----- src/shared/config.h | 2 +- src/shared/file_receive.c | 10 ++++++++-- src/shared/metadata.c | 17 +++++++++++++---- tests/test_client_cli.c | 19 +++++++++++++++++++ tests/test_config.c | 3 +++ 8 files changed, 55 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fdbd9f3..6962093 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,7 @@ defaults to the current directory. | ## Protocol and Security -FastSync protocol version `2.2.0` is shared by the client and server. The +FastSync protocol version `2.3.0` is shared by the client and server. The current protocol is sender-driven and includes configuration negotiation, incremental checks, checksums, manifests, keep-alives, abort handling, and FastSync-native delta messages. Client and server versions must currently diff --git a/src/server/server.c b/src/server/server.c index af5cd43..c28064c 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -232,6 +232,7 @@ void handler(int file_descriptor) { protocol_session_unbind(); return; } + log_set_8_bit_output(config->eight_bit_output); if (!authorized_root) { log_message(LOG_LEVEL_ERROR, "No server-side destination root configured"); config_delete(config); diff --git a/src/shared/config.c b/src/shared/config.c index 6468503..055003e 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -130,6 +130,7 @@ static bool validate_received_config(const Config* config) { valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) && + valid_wire_bool(config->eight_bit_output) && (!config->use_compression || (config->compression_level >= 1 && config->compression_level <= 22)) && config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE && @@ -219,11 +220,11 @@ void config_delete(Config* config) { * helper call order in config_send and config_receive unchanged when adding * fields. */ static bool send_core_fields(int fd, const Config* c) { - return send_str(fd, c->version) && send_str(fd, c->send_directory) && - send_str(fd, c->receive_root_directory) && send_int(fd, c->save_to_disk) && - send_int(fd, c->use_multithreading) && send_int(fd, c->use_chunk_serialization) && - send_int(fd, c->use_compression) && send_int(fd, c->use_metadata) && - send_int(fd, c->compression_level) && + return send_str(fd, c->version) && send_int(fd, c->eight_bit_output) && + send_str(fd, c->send_directory) && send_str(fd, c->receive_root_directory) && + send_int(fd, c->save_to_disk) && send_int(fd, c->use_multithreading) && + send_int(fd, c->use_chunk_serialization) && send_int(fd, c->use_compression) && + send_int(fd, c->use_metadata) && send_int(fd, c->compression_level) && send_n_data(fd, &c->chunk_size, sizeof(c->chunk_size)) && send_int(fd, c->use_sendfile); } @@ -259,6 +260,9 @@ static bool send_resume_options(int fd, const Config* c) { static bool receive_core_fields(int fd, Config* c) { int value; + if (!receive_wire_bool(fd, &c->eight_bit_output)) + return false; + log_set_8_bit_output(c->eight_bit_output); c->send_directory = receive_str(fd); c->receive_root_directory = receive_str(fd); if (!c->send_directory || !c->receive_root_directory) diff --git a/src/shared/config.h b/src/shared/config.h index 92df6ea..4d7c957 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -129,7 +129,7 @@ typedef struct Config { char* compress_choice; } Config; -#define PROTOCOL_VERSION "2.2.0" +#define PROTOCOL_VERSION "2.3.0" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) Config* config_create(void); diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index a610d67..caf9e1f 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -332,7 +332,10 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) { } if (has_path_traversal(check_path)) { - log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path); + char* escaped_path = output_escape(check_path, log_get_8_bit_output()); + log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", + escaped_path ? escaped_path : ""); + free(escaped_path); free(check_path); return NULL; } @@ -479,7 +482,10 @@ File* file_receive(const Config* config, int file_descriptor) { if (path == NULL) return NULL; if (path[0] == '\0' || has_path_traversal(path)) { - log_message(LOG_LEVEL_ERROR, "Invalid received file path: %s", path); + char* escaped_path = output_escape(path, log_get_8_bit_output()); + log_message(LOG_LEVEL_ERROR, "Invalid received file path: %s", + escaped_path ? escaped_path : ""); + free(escaped_path); free(path); return NULL; } diff --git a/src/shared/metadata.c b/src/shared/metadata.c index 0c37309..30b3c45 100644 --- a/src/shared/metadata.c +++ b/src/shared/metadata.c @@ -2,6 +2,7 @@ #include "file.h" #include "log.h" #include "protocol.h" +#include "utils.h" #include #include #include @@ -183,8 +184,12 @@ void file_restore_metadata(const char* path, const FileMetadata* metadata) { if (metadata == NULL) return; mode_t safe_mode = metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH); - if (chmod(path, safe_mode) != 0) - log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", path, strerror(errno)); + if (chmod(path, safe_mode) != 0) { + char* escaped_path = output_escape(path, log_get_8_bit_output()); + log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", + escaped_path ? escaped_path : "", strerror(errno)); + free(escaped_path); + } /* Never apply client-supplied ownership. The descriptor API below is the receiver write path; retain this legacy API only for compatibility. */ struct timespec times[2]; @@ -192,8 +197,12 @@ void file_restore_metadata(const char* path, const FileMetadata* metadata) { times[0].tv_nsec = UTIME_OMIT; times[1].tv_sec = metadata->mtime_sec; times[1].tv_nsec = metadata->mtime_nsec; - if (utimensat(AT_FDCWD, path, times, 0) != 0) - log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s", path, strerror(errno)); + if (utimensat(AT_FDCWD, path, times, 0) != 0) { + char* escaped_path = output_escape(path, log_get_8_bit_output()); + log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s", + escaped_path ? escaped_path : "", strerror(errno)); + free(escaped_path); + } } bool file_restore_metadata_fd(int fd, const FileMetadata* metadata) { diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 55b99dd..26b42c0 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -338,6 +338,24 @@ static void test_parse_args_archive() { config_delete(cfg); } +static void test_parse_args_8_bit_output() { + Config* cfg = config_create(); + char* long_argv[] = {"fastsync", "--8-bit-output", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 4, long_argv, positional_args, &positional_count), 0); + EXPECT_TRUE(cfg->eight_bit_output); + config_delete(cfg); + + cfg = config_create(); + char* short_argv[] = {"fastsync", "-8", "/src", "/dst"}; + positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 4, short_argv, positional_args, &positional_count), 0); + EXPECT_TRUE(cfg->eight_bit_output); + config_delete(cfg); +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -359,4 +377,5 @@ void test_client_cli() { test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); + test_parse_args_8_bit_output(); } diff --git a/tests/test_config.c b/tests/test_config.c index 4f124db..4e4a000 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -126,6 +126,7 @@ static void test_config_send_receive() { send_cfg->use_metadata = true; send_cfg->compression_level = 5; send_cfg->chunk_size = 1024; + send_cfg->eight_bit_output = true; /* Use socketpair for bidirectional communication */ int p[2]; @@ -160,6 +161,8 @@ static void test_config_send_receive() { ok = false; if (recv_cfg->chunk_size != 1024) ok = false; + if (!recv_cfg->eight_bit_output) + ok = false; } config_delete(recv_cfg); close(p[0]); From 066c7ed1afd6c3e37b5d5d3aeffb7c6b96662cc1 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 22:40:30 +0200 Subject: [PATCH 3/3] fix: address 8-bit output re-review findings --- src/client/client_cli.c | 27 +++++++++++++++++++------- src/server/server.c | 16 ++++++++++++---- src/shared/config.c | 19 +++++++++++++------ src/shared/log.c | 2 +- src/shared/protocol.c | 19 +++++++++++++++++-- src/shared/protocol.h | 3 +++ src/shared/transport_ssh.c | 9 +++++++-- src/shared/transport_tcp.c | 6 +++++- src/shared/transport_tls.c | 16 +++++++++++++--- tests/test_shared_utils.c | 39 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 130 insertions(+), 26 deletions(-) diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 266ec64..c2b5d11 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -211,7 +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); + protocol_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) { @@ -226,7 +226,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, return -1; } if (entry->offset == offsetof(Config, eight_bit_output)) - log_set_8_bit_output(true); + protocol_set_8_bit_output(true); continue; } @@ -302,7 +302,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization"); } else if (opt_is(argv[i], "--server-port", NULL) && i + 1 < argc) { if (!parse_positive_int(argv[++i], &config->server_port)) { - log_message(LOG_LEVEL_ERROR, "invalid --server-port value: %s", argv[i]); + char* escaped = output_escape(argv[i], false); + log_message(LOG_LEVEL_ERROR, "invalid --server-port value: %s", + escaped ? escaped : ""); + free(escaped); return -1; } if (config->server_port > 65535) { @@ -340,7 +343,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } FILE* lf = fopen(argv[++i], "a"); if (!lf) { - log_message(LOG_LEVEL_ERROR, "could not open log file '%s': %s", argv[i], strerror(errno)); + char* escaped = output_escape(argv[i], false); + log_message(LOG_LEVEL_ERROR, "could not open log file '%s': %s", + escaped ? escaped : "", strerror(errno)); + free(escaped); return -1; } config->log_file = lf; @@ -366,14 +372,18 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, return -1; } } else if (argv[i][0] == '-') { - fprintf(stderr, "Unknown option: %s\n", argv[i]); + char* escaped = output_escape(argv[i], false); + fprintf(stderr, "Unknown option: %s\n", escaped ? escaped : ""); + free(escaped); print_usage(); return -1; } else { if (*positional_count < 2) positional_args[(*positional_count)++] = i; else { - fprintf(stderr, "Unexpected argument: %s\n", argv[i]); + char* escaped = output_escape(argv[i], false); + fprintf(stderr, "Unexpected argument: %s\n", escaped ? escaped : ""); + free(escaped); print_usage(); return -1; } @@ -385,7 +395,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) { FILE* fp = fopen(filepath, "r"); if (!fp) { - log_message(LOG_LEVEL_ERROR, "could not open pattern file '%s': %s", filepath, strerror(errno)); + char* escaped = output_escape(filepath, false); + log_message(LOG_LEVEL_ERROR, "could not open pattern file '%s': %s", + escaped ? escaped : "", strerror(errno)); + free(escaped); return -1; } char* line = NULL; diff --git a/src/server/server.c b/src/server/server.c index c28064c..8aa02e9 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -232,7 +232,7 @@ void handler(int file_descriptor) { protocol_session_unbind(); return; } - log_set_8_bit_output(config->eight_bit_output); + protocol_set_8_bit_output(config->eight_bit_output); if (!authorized_root) { log_message(LOG_LEVEL_ERROR, "No server-side destination root configured"); config_delete(config); @@ -401,12 +401,17 @@ int main(int argc, char* argv[]) { char* end; long p = strtol(argv[++i], &end, 10); if (*end || p <= 0 || p > 65535) { - fprintf(stderr, "Error: invalid port '%s' (must be 1-65535)\n", argv[i]); + char* escaped = output_escape(argv[i], false); + fprintf(stderr, "Error: invalid port '%s' (must be 1-65535)\n", + escaped ? escaped : ""); + free(escaped); return 1; } port = (int)p; } else if (argv[i][0] == '-') { - fprintf(stderr, "Unknown option: %s\n", argv[i]); + char* escaped = output_escape(argv[i], false); + fprintf(stderr, "Unknown option: %s\n", escaped ? escaped : ""); + free(escaped); print_server_usage(); return 1; } @@ -416,7 +421,10 @@ int main(int argc, char* argv[]) { signal(SIGINT, cleanup); signal(SIGTERM, cleanup); if (!configure_authorization(destination_root)) { - fprintf(stderr, "Error: invalid destination root '%s'\n", destination_root); + char* escaped = output_escape(destination_root, false); + fprintf(stderr, "Error: invalid destination root '%s'\n", + escaped ? escaped : ""); + free(escaped); return 1; } if (stdio_mode) { diff --git a/src/shared/config.c b/src/shared/config.c index 055003e..27d8f0e 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -220,8 +220,10 @@ void config_delete(Config* config) { * helper call order in config_send and config_receive unchanged when adding * fields. */ static bool send_core_fields(int fd, const Config* c) { - return send_str(fd, c->version) && send_int(fd, c->eight_bit_output) && - send_str(fd, c->send_directory) && send_str(fd, c->receive_root_directory) && + if (!send_str(fd, c->version) || !send_int(fd, c->eight_bit_output)) + return false; + protocol_set_8_bit_output(c->eight_bit_output); + return send_str(fd, c->send_directory) && send_str(fd, c->receive_root_directory) && send_int(fd, c->save_to_disk) && send_int(fd, c->use_multithreading) && send_int(fd, c->use_chunk_serialization) && send_int(fd, c->use_compression) && send_int(fd, c->use_metadata) && send_int(fd, c->compression_level) && @@ -262,7 +264,7 @@ static bool receive_core_fields(int fd, Config* c) { int value; if (!receive_wire_bool(fd, &c->eight_bit_output)) return false; - log_set_8_bit_output(c->eight_bit_output); + protocol_set_8_bit_output(c->eight_bit_output); c->send_directory = receive_str(fd); c->receive_root_directory = receive_str(fd); if (!c->send_directory || !c->receive_root_directory) @@ -363,8 +365,10 @@ Config* config_receive(int file_descriptor) { if (!config->version) goto error; if (strcmp(config->version, PROTOCOL_VERSION) != 0) { - fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", config->version, - PROTOCOL_VERSION); + char* escaped_version = output_escape(config->version, false); + fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", + escaped_version ? escaped_version : "", PROTOCOL_VERSION); + free(escaped_version); send_status(file_descriptor, STATUS_ERROR); goto error; } @@ -376,7 +380,10 @@ Config* config_receive(int file_descriptor) { goto error; if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 && strcmp(config->compress_choice, "none") != 0) { - fprintf(stderr, "Unsupported compression choice: %s\n", config->compress_choice); + char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output); + fprintf(stderr, "Unsupported compression choice: %s\n", + escaped_choice ? escaped_choice : ""); + free(escaped_choice); send_status(file_descriptor, STATUS_ERROR); goto error; } diff --git a/src/shared/log.c b/src/shared/log.c index bf11296..78ef699 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -8,7 +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; +static _Thread_local bool eight_bit_output; void set_log_level(LogLevel level) { current_log_level = level; diff --git a/src/shared/protocol.c b/src/shared/protocol.c index 728578a..ae37a95 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -45,6 +45,7 @@ void io_set_fds(int read_fd, int write_fd) { legacy_io_session.read_fd = read_fd; legacy_io_session.write_fd = write_fd; legacy_io_session.ssl = NULL; + legacy_io_session.eight_bit_output = false; legacy_io_session.total_allocated_bytes = 0; protocol_session_set_bwlimit(&legacy_io_session, global_bwlimit()); } @@ -60,6 +61,7 @@ void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd) void protocol_session_bind(ProtocolSession* session) { bound_session = session; + log_set_8_bit_output(session && session->eight_bit_output); } void protocol_session_unbind(void) { @@ -104,6 +106,19 @@ void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long b session->bw_last_refill_nsec = now.tv_nsec; } +void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled) { + if (!session) + return; + session->eight_bit_output = enabled; + if (session == bound_session) + log_set_8_bit_output(enabled); +} + +void protocol_set_8_bit_output(bool enabled) { + ProtocolSession* session = bound_session ? bound_session : &legacy_io_session; + protocol_session_set_8_bit_output(session, enabled); +} + static void bw_throttle_session(ProtocolSession* session, size_t bytes_written) { if (session->bwlimit == 0) return; @@ -329,7 +344,7 @@ bool protocol_send_str(ProtocolSession* session, const char* data) { return false; if (!protocol_send_n_data(session, data, size)) return false; - char* escaped = output_escape(data, log_get_8_bit_output()); + char* escaped = output_escape(data, session->eight_bit_output); log_message(LOG_LEVEL_DEBUG, "Send String: %s", escaped ? escaped : ""); free(escaped); return true; @@ -359,7 +374,7 @@ char* protocol_receive_str(ProtocolSession* session) { } data[size] = '\0'; session->total_allocated_bytes += size + 1; - char* escaped = output_escape(data, log_get_8_bit_output()); + char* escaped = output_escape(data, session->eight_bit_output); log_message(LOG_LEVEL_DEBUG, "Received String: %s", escaped ? escaped : ""); free(escaped); return data; diff --git a/src/shared/protocol.h b/src/shared/protocol.h index 8db196a..8310692 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -36,6 +36,7 @@ typedef struct ProtocolSession { long long bw_last_refill_sec; long bw_last_refill_nsec; unsigned long long total_allocated_bytes; + bool eight_bit_output; } ProtocolSession; typedef int Status; @@ -65,6 +66,8 @@ void protocol_session_bind(ProtocolSession* session); void protocol_session_unbind(void); void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl); void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec); +void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled); +void protocol_set_8_bit_output(bool enabled); bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t data_size); bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size); bool protocol_send_str(ProtocolSession* session, const char* data); diff --git a/src/shared/transport_ssh.c b/src/shared/transport_ssh.c index 4941526..9e663c7 100644 --- a/src/shared/transport_ssh.c +++ b/src/shared/transport_ssh.c @@ -71,7 +71,9 @@ static int parse_remote_dest(const char* dest, RemoteDest* r) { Client* client_connect_ssh(const char* destination, int port, const char* server_path) { RemoteDest r; if (parse_remote_dest(destination, &r) != 0) { - fprintf(stderr, "Invalid remote destination: %s\n", destination); + char* escaped = output_escape(destination, false); + fprintf(stderr, "Invalid remote destination: %s\n", escaped ? escaped : ""); + free(escaped); return NULL; } @@ -169,8 +171,11 @@ Client* client_connect_ssh(const char* destination, int port, const char* server close(sv[0]); waitpid(pid, NULL, 0); remote_dest_destroy(&r); + const char* path = server_path ? server_path : "fastsync-server"; + char* escaped = output_escape(path, false); fprintf(stderr, "Error: could not launch '%s --stdio' on remote\n", - server_path ? server_path : "fastsync-server"); + escaped ? escaped : ""); + free(escaped); return NULL; } diff --git a/src/shared/transport_tcp.c b/src/shared/transport_tcp.c index 3c9c9a1..953e846 100644 --- a/src/shared/transport_tcp.c +++ b/src/shared/transport_tcp.c @@ -1,6 +1,7 @@ #include "transport_tcp.h" #include "log.h" #include "protocol.h" +#include "utils.h" #include #include #include @@ -190,7 +191,10 @@ bool tcp_connect_socket(Client* client, char* host, int port) { int err = getaddrinfo(host, port_str, &hints, &result); if (err != 0 || result == NULL) { - fprintf(stderr, "Could not resolve host: %s (%s)\n", host, gai_strerror(err)); + char* escaped_host = output_escape(host, false); + fprintf(stderr, "Could not resolve host: %s (%s)\n", + escaped_host ? escaped_host : "", gai_strerror(err)); + free(escaped_host); return false; } diff --git a/src/shared/transport_tls.c b/src/shared/transport_tls.c index ec89f4e..fa21e95 100644 --- a/src/shared/transport_tls.c +++ b/src/shared/transport_tls.c @@ -2,6 +2,7 @@ #include "log.h" #include "protocol.h" #include "transport_tcp.h" +#include "utils.h" #include #include #include @@ -65,13 +66,19 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key return NULL; } if (SSL_CTX_use_certificate_file(ctx, cert, SSL_FILETYPE_PEM) <= 0) { - log_message(LOG_LEVEL_ERROR, "Failed to load certificate: %s", cert); + char* escaped = output_escape(cert, false); + log_message(LOG_LEVEL_ERROR, "Failed to load certificate: %s", + escaped ? escaped : ""); + free(escaped); log_ssl_errors(); SSL_CTX_free(ctx); return NULL; } if (SSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM) <= 0) { - log_message(LOG_LEVEL_ERROR, "Failed to load private key: %s", key); + char* escaped = output_escape(key, false); + log_message(LOG_LEVEL_ERROR, "Failed to load private key: %s", + escaped ? escaped : ""); + free(escaped); log_ssl_errors(); SSL_CTX_free(ctx); return NULL; @@ -85,7 +92,10 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key if (ca_path) { if (!SSL_CTX_load_verify_locations(ctx, ca_path, NULL)) { - log_message(LOG_LEVEL_ERROR, "Failed to load CA: %s", ca_path); + char* escaped = output_escape(ca_path, false); + log_message(LOG_LEVEL_ERROR, "Failed to load CA: %s", + escaped ? escaped : ""); + free(escaped); log_ssl_errors(); SSL_CTX_free(ctx); return NULL; diff --git a/tests/test_shared_utils.c b/tests/test_shared_utils.c index 79813d8..45276da 100644 --- a/tests/test_shared_utils.c +++ b/tests/test_shared_utils.c @@ -1,8 +1,27 @@ #include "test_shared_utils.h" #include "utils.h" +#include "protocol.h" #include "test_utils.h" #include #include +#include + +typedef struct { + bool eight_bit_output; + const char* expected; + int failed; +} EscapeThreadArgs; + +static int escape_thread(void* arg) { + EscapeThreadArgs* args = arg; + for (int i = 0; i < 1000; i++) { + char* escaped = output_escape("x\xc3\xa9\n", args->eight_bit_output); + if (!escaped || strcmp(escaped, args->expected) != 0) + args->failed = 1; + free(escaped); + } + return 0; +} void test_shared_utils() { char high_bit[] = {'a', (char)0xc3, (char)0xa9, '\n', '\0'}; @@ -13,6 +32,26 @@ void test_shared_utils() { EXPECT_EQ_STR(escaped, "a\xc3\xa9\\#012"); free(escaped); + ProtocolSession safe_session; + ProtocolSession eight_bit_session; + protocol_session_init(&safe_session, -1, -1); + protocol_session_init(&eight_bit_session, -1, -1); + protocol_session_set_8_bit_output(&safe_session, false); + protocol_session_set_8_bit_output(&eight_bit_session, true); + EXPECT_FALSE(safe_session.eight_bit_output); + EXPECT_TRUE(eight_bit_session.eight_bit_output); + + EscapeThreadArgs safe_args = {false, "x\\#303\\#251\\#012", 0}; + EscapeThreadArgs eight_bit_args = {true, "x\xc3\xa9\\#012", 0}; + thrd_t safe_thread; + thrd_t eight_bit_thread; + EXPECT_EQ_INT(thrd_create(&safe_thread, escape_thread, &safe_args), thrd_success); + EXPECT_EQ_INT(thrd_create(&eight_bit_thread, escape_thread, &eight_bit_args), thrd_success); + EXPECT_EQ_INT(thrd_join(safe_thread, NULL), thrd_success); + EXPECT_EQ_INT(thrd_join(eight_bit_thread, NULL), thrd_success); + EXPECT_FALSE(safe_args.failed); + EXPECT_FALSE(eight_bit_args.failed); + // Test str_dup const char* dup_null = str_dup(NULL); EXPECT_NULL(dup_null);