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]);