Merge remote-tracking branch 'origin/feat/8-bit-output' into dev

# Conflicts:
#	src/client/client_cli.c
#	src/client/client_send.c
#	src/shared/config.c
#	tests/test_client_cli.c
#	tests/test_config.c
#	tests/test_shared_utils.c
This commit is contained in:
2026-09-04 17:18:01 +02:00
21 changed files with 248 additions and 35 deletions
+1 -1
View File
@@ -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 | | `--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=FILE` | Log to file | ✅ Implemented | `log_file` config field |
| `--log-file-format=FMT` | Log format | ❌ Not Implemented | | | `--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 | | `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect |
## 3. File Selection ## 3. File Selection
+22 -5
View File
@@ -156,6 +156,7 @@ static const OptionEntry OPTION_TABLE[] = {
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)}, {"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
{"--fsync", NULL, OPT_FLAG, offsetof(Config, use_fsync)}, {"--fsync", NULL, OPT_FLAG, offsetof(Config, use_fsync)},
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)}, {"--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)}, {"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)},
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)}, {"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
@@ -214,6 +215,7 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch
int parse_args(Config* config, int argc, char* argv[], int* positional_args, int parse_args(Config* config, int argc, char* argv[], int* positional_args,
int* positional_count) { int* positional_count) {
bool verbose = false; bool verbose = false;
protocol_set_8_bit_output(config->eight_bit_output);
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
const OptionEntry* entry = find_table_option(argv[i]); const OptionEntry* entry = find_table_option(argv[i]);
if (entry) { if (entry) {
@@ -227,6 +229,8 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
} else if (apply_table_option(config, entry, NULL) != 0) { } else if (apply_table_option(config, entry, NULL) != 0) {
return -1; return -1;
} }
if (entry->offset == offsetof(Config, eight_bit_output))
protocol_set_8_bit_output(true);
continue; continue;
} }
@@ -302,7 +306,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization"); log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization");
} else if (opt_is(argv[i], "--server-port", NULL) && i + 1 < argc) { } else if (opt_is(argv[i], "--server-port", NULL) && i + 1 < argc) {
if (!parse_positive_int(argv[++i], &config->server_port)) { 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 : "<allocation failed>");
free(escaped);
return -1; return -1;
} }
if (config->server_port > 65535) { if (config->server_port > 65535) {
@@ -340,7 +347,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
} }
FILE* lf = fopen(argv[++i], "a"); FILE* lf = fopen(argv[++i], "a");
if (!lf) { 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 : "<allocation failed>", strerror(errno));
free(escaped);
return -1; return -1;
} }
config->log_file = lf; config->log_file = lf;
@@ -368,14 +378,18 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
return -1; return -1;
} }
} else if (argv[i][0] == '-') { } 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 : "<allocation failed>");
free(escaped);
print_usage(); print_usage();
return -1; return -1;
} else { } else {
if (*positional_count < 2) if (*positional_count < 2)
positional_args[(*positional_count)++] = i; positional_args[(*positional_count)++] = i;
else { else {
fprintf(stderr, "Unexpected argument: %s\n", argv[i]); char* escaped = output_escape(argv[i], false);
fprintf(stderr, "Unexpected argument: %s\n", escaped ? escaped : "<allocation failed>");
free(escaped);
print_usage(); print_usage();
return -1; return -1;
} }
@@ -388,7 +402,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) { static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) {
FILE* fp = fopen(filepath, "r"); FILE* fp = fopen(filepath, "r");
if (!fp) { 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 : "<allocation failed>", strerror(errno));
free(escaped);
return -1; return -1;
} }
char* line = NULL; char* line = NULL;
+9 -2
View File
@@ -233,11 +233,18 @@ static int send_dry_run_manifest(const Config* config) {
while ((chunk = directory_scanner_next(scanner)) != NULL) { while ((chunk = directory_scanner_next(scanner)) != NULL) {
for (int i = 0; i < chunk->element_count; i++) { for (int i = 0; i < chunk->element_count; i++) {
if (!config->quiet) { if (!config->quiet) {
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;
}
if (config->human_readable) if (config->human_readable)
printf(" %s (%s)\n", chunk->items[i]->path, printf(" %s (%s)\n", escaped_path,
display_bytes(chunk->items[i]->data->size, true, size_buffer, sizeof(size_buffer))); display_bytes(chunk->items[i]->data->size, true, size_buffer, sizeof(size_buffer)));
else else
printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); printf(" %s (%zu bytes)\n", escaped_path, chunk->items[i]->data->size);
free(escaped_path);
} }
total_bytes += chunk->items[i]->data->size; total_bytes += chunk->items[i]->data->size;
file_count++; file_count++;
+1
View File
@@ -21,6 +21,7 @@ void print_usage(void) {
printf(" --remove-source-files Remove regular source files after successful transfer\n"); printf(" --remove-source-files Remove regular source files after successful transfer\n");
printf(" -p <port> SSH port (default: 22)\n"); printf(" -p <port> SSH port (default: 22)\n");
printf(" --progress Show transfer progress\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(" --delete Delete files on receiver not in source\n");
printf(" --exclude <pattern> Exclude files matching pattern\n"); printf(" --exclude <pattern> Exclude files matching pattern\n");
printf(" --include <pattern> Only include files matching pattern\n"); printf(" --include <pattern> Only include files matching pattern\n");
+12 -3
View File
@@ -232,6 +232,7 @@ void handler(int file_descriptor) {
protocol_session_unbind(); protocol_session_unbind();
return; return;
} }
protocol_set_8_bit_output(config->eight_bit_output);
if (!authorized_root) { if (!authorized_root) {
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured"); log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
config_delete(config); config_delete(config);
@@ -400,12 +401,17 @@ int main(int argc, char* argv[]) {
char* end; char* end;
long p = strtol(argv[++i], &end, 10); long p = strtol(argv[++i], &end, 10);
if (*end || p <= 0 || p > 65535) { 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 : "<allocation failed>");
free(escaped);
return 1; return 1;
} }
port = (int)p; port = (int)p;
} else if (argv[i][0] == '-') { } 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 : "<allocation failed>");
free(escaped);
print_server_usage(); print_server_usage();
return 1; return 1;
} }
@@ -415,7 +421,10 @@ int main(int argc, char* argv[]) {
signal(SIGINT, cleanup); signal(SIGINT, cleanup);
signal(SIGTERM, cleanup); signal(SIGTERM, cleanup);
if (!configure_authorization(destination_root)) { 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 : "<allocation failed>");
free(escaped);
return 1; return 1;
} }
if (stdio_mode) { if (stdio_mode) {
+20 -9
View File
@@ -69,6 +69,7 @@ static void config_set_defaults(Config* config) {
config->debug_level = 0; config->debug_level = 0;
config->list_only = false; config->list_only = false;
config->human_readable = false; config->human_readable = false;
config->eight_bit_output = false;
config->update = false; config->update = false;
config->inplace = false; config->inplace = false;
config->use_fsync = false; config->use_fsync = false;
@@ -131,7 +132,7 @@ static bool validate_received_config(const Config* config) {
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) && valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) &&
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) && 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->partial) && valid_wire_bool(config->delete_before) &&
valid_wire_bool(config->checksum) && valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
(!config->use_compression || (!config->use_compression ||
(config->compression_level >= 1 && config->compression_level <= 22)) && (config->compression_level >= 1 && config->compression_level <= 22)) &&
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE && config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
@@ -221,11 +222,13 @@ void config_delete(Config* config) {
* helper call order in config_send and config_receive unchanged when adding * helper call order in config_send and config_receive unchanged when adding
* fields. */ * fields. */
static bool send_core_fields(int fd, const Config* c) { static bool send_core_fields(int fd, const Config* c) {
return send_str(fd, c->version) && send_str(fd, c->send_directory) && if (!send_str(fd, c->version) || !send_int(fd, c->eight_bit_output))
send_str(fd, c->receive_root_directory) && send_int(fd, c->save_to_disk) && return false;
send_int(fd, c->use_multithreading) && send_int(fd, c->use_chunk_serialization) && protocol_set_8_bit_output(c->eight_bit_output);
send_int(fd, c->use_compression) && send_int(fd, c->use_metadata) && return send_str(fd, c->send_directory) && send_str(fd, c->receive_root_directory) &&
send_int(fd, c->compression_level) && 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); send_n_data(fd, &c->chunk_size, sizeof(c->chunk_size)) && send_int(fd, c->use_sendfile);
} }
@@ -262,6 +265,9 @@ static bool send_resume_options(int fd, const Config* c) {
static bool receive_core_fields(int fd, Config* c) { static bool receive_core_fields(int fd, Config* c) {
int value; int value;
if (!receive_wire_bool(fd, &c->eight_bit_output))
return false;
protocol_set_8_bit_output(c->eight_bit_output);
c->send_directory = receive_str(fd); c->send_directory = receive_str(fd);
c->receive_root_directory = receive_str(fd); c->receive_root_directory = receive_str(fd);
if (!c->send_directory || !c->receive_root_directory) if (!c->send_directory || !c->receive_root_directory)
@@ -362,8 +368,10 @@ Config* config_receive(int file_descriptor) {
if (!config->version) if (!config->version)
goto error; goto error;
if (strcmp(config->version, PROTOCOL_VERSION) != 0) { if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", config->version, char* escaped_version = output_escape(config->version, false);
PROTOCOL_VERSION); fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n",
escaped_version ? escaped_version : "<allocation failed>", PROTOCOL_VERSION);
free(escaped_version);
send_status(file_descriptor, STATUS_ERROR); send_status(file_descriptor, STATUS_ERROR);
goto error; goto error;
} }
@@ -375,7 +383,10 @@ Config* config_receive(int file_descriptor) {
goto error; goto error;
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 && if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
strcmp(config->compress_choice, "none") != 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 : "<allocation failed>");
free(escaped_choice);
send_status(file_descriptor, STATUS_ERROR); send_status(file_descriptor, STATUS_ERROR);
goto error; goto error;
} }
+1
View File
@@ -75,6 +75,7 @@ typedef struct Config {
int debug_level; int debug_level;
bool list_only; bool list_only;
bool human_readable; bool human_readable;
bool eight_bit_output;
// Issue #127: Transfer modes // Issue #127: Transfer modes
bool update; bool update;
+8 -2
View File
@@ -332,7 +332,10 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
} }
if (has_path_traversal(check_path)) { 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 : "<allocation failed>");
free(escaped_path);
free(check_path); free(check_path);
return NULL; return NULL;
} }
@@ -479,7 +482,10 @@ File* file_receive(const Config* config, int file_descriptor) {
if (path == NULL) if (path == NULL)
return NULL; return NULL;
if (path[0] == '\0' || has_path_traversal(path)) { 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 : "<allocation failed>");
free(escaped_path);
free(path); free(path);
return NULL; return NULL;
} }
+9
View File
@@ -8,6 +8,7 @@
static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
static LogLevel current_log_level = LOG_LEVEL_WARNING; static LogLevel current_log_level = LOG_LEVEL_WARNING;
static FILE* log_fp = NULL; static FILE* log_fp = NULL;
static _Thread_local bool eight_bit_output;
void set_log_level(LogLevel level) { void set_log_level(LogLevel level) {
current_log_level = level; current_log_level = level;
@@ -17,6 +18,14 @@ void log_set_file(FILE* fp) {
log_fp = 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, static inline void write_message(FILE* dest_io, LogLevel log_level, struct tm t, const char* format,
va_list args) { va_list args) {
fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1,
+3
View File
@@ -2,6 +2,7 @@
#define LOG_H #define LOG_H
#include <stdio.h> #include <stdio.h>
#include <stdbool.h>
typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; 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 log_perror(const char* context);
void set_log_level(LogLevel level); void set_log_level(LogLevel level);
void log_set_file(FILE* fp); void log_set_file(FILE* fp);
void log_set_8_bit_output(bool enabled);
bool log_get_8_bit_output(void);
#endif #endif
+13 -4
View File
@@ -2,6 +2,7 @@
#include "file.h" #include "file.h"
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include "utils.h"
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <stdint.h> #include <stdint.h>
@@ -183,8 +184,12 @@ void file_restore_metadata(const char* path, const FileMetadata* metadata) {
if (metadata == NULL) if (metadata == NULL)
return; return;
mode_t safe_mode = metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH); mode_t safe_mode = metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH);
if (chmod(path, safe_mode) != 0) if (chmod(path, safe_mode) != 0) {
log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", path, strerror(errno)); 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 : "<allocation failed>", strerror(errno));
free(escaped_path);
}
/* Never apply client-supplied ownership. The descriptor API below is the /* Never apply client-supplied ownership. The descriptor API below is the
receiver write path; retain this legacy API only for compatibility. */ receiver write path; retain this legacy API only for compatibility. */
struct timespec times[2]; 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[0].tv_nsec = UTIME_OMIT;
times[1].tv_sec = metadata->mtime_sec; times[1].tv_sec = metadata->mtime_sec;
times[1].tv_nsec = metadata->mtime_nsec; times[1].tv_nsec = metadata->mtime_nsec;
if (utimensat(AT_FDCWD, path, times, 0) != 0) if (utimensat(AT_FDCWD, path, times, 0) != 0) {
log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s", path, strerror(errno)); 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 : "<allocation failed>", strerror(errno));
free(escaped_path);
}
} }
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata) { bool file_restore_metadata_fd(int fd, const FileMetadata* metadata) {
+22 -2
View File
@@ -1,5 +1,6 @@
#include "protocol.h" #include "protocol.h"
#include "log.h" #include "log.h"
#include "utils.h"
#include <errno.h> #include <errno.h>
#include <limits.h> #include <limits.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
@@ -44,6 +45,7 @@ void io_set_fds(int read_fd, int write_fd) {
legacy_io_session.read_fd = read_fd; legacy_io_session.read_fd = read_fd;
legacy_io_session.write_fd = write_fd; legacy_io_session.write_fd = write_fd;
legacy_io_session.ssl = NULL; legacy_io_session.ssl = NULL;
legacy_io_session.eight_bit_output = false;
legacy_io_session.total_allocated_bytes = 0; legacy_io_session.total_allocated_bytes = 0;
protocol_session_set_bwlimit(&legacy_io_session, global_bwlimit()); protocol_session_set_bwlimit(&legacy_io_session, global_bwlimit());
} }
@@ -59,6 +61,7 @@ void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd)
void protocol_session_bind(ProtocolSession* session) { void protocol_session_bind(ProtocolSession* session) {
bound_session = session; bound_session = session;
log_set_8_bit_output(session && session->eight_bit_output);
} }
void protocol_session_unbind(void) { void protocol_session_unbind(void) {
@@ -103,6 +106,19 @@ void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long b
session->bw_last_refill_nsec = now.tv_nsec; 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) { static void bw_throttle_session(ProtocolSession* session, size_t bytes_written) {
if (session->bwlimit == 0) if (session->bwlimit == 0)
return; return;
@@ -328,7 +344,9 @@ bool protocol_send_str(ProtocolSession* session, const char* data) {
return false; return false;
if (!protocol_send_n_data(session, data, size)) if (!protocol_send_n_data(session, data, size))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Send String: %s", data); char* escaped = output_escape(data, session->eight_bit_output);
log_message(LOG_LEVEL_DEBUG, "Send String: %s", escaped ? escaped : "<allocation failed>");
free(escaped);
return true; return true;
} }
@@ -356,7 +374,9 @@ char* protocol_receive_str(ProtocolSession* session) {
} }
data[size] = '\0'; data[size] = '\0';
session->total_allocated_bytes += size + 1; session->total_allocated_bytes += size + 1;
log_message(LOG_LEVEL_DEBUG, "Received String: %s", data); char* escaped = output_escape(data, session->eight_bit_output);
log_message(LOG_LEVEL_DEBUG, "Received String: %s", escaped ? escaped : "<allocation failed>");
free(escaped);
return data; return data;
} }
+3
View File
@@ -36,6 +36,7 @@ typedef struct ProtocolSession {
long long bw_last_refill_sec; long long bw_last_refill_sec;
long bw_last_refill_nsec; long bw_last_refill_nsec;
unsigned long long total_allocated_bytes; unsigned long long total_allocated_bytes;
bool eight_bit_output;
} ProtocolSession; } ProtocolSession;
typedef int Status; typedef int Status;
@@ -65,6 +66,8 @@ void protocol_session_bind(ProtocolSession* session);
void protocol_session_unbind(void); void protocol_session_unbind(void);
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl); 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_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_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_receive_n_data(ProtocolSession* session, void* data, size_t data_size);
bool protocol_send_str(ProtocolSession* session, const char* data); bool protocol_send_str(ProtocolSession* session, const char* data);
+7 -2
View File
@@ -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) { Client* client_connect_ssh(const char* destination, int port, const char* server_path) {
RemoteDest r; RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) { 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 : "<allocation failed>");
free(escaped);
return NULL; return NULL;
} }
@@ -169,8 +171,11 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
close(sv[0]); close(sv[0]);
waitpid(pid, NULL, 0); waitpid(pid, NULL, 0);
remote_dest_destroy(&r); 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", fprintf(stderr, "Error: could not launch '%s --stdio' on remote\n",
server_path ? server_path : "fastsync-server"); escaped ? escaped : "<allocation failed>");
free(escaped);
return NULL; return NULL;
} }
+5 -1
View File
@@ -1,6 +1,7 @@
#include "transport_tcp.h" #include "transport_tcp.h"
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include "utils.h"
#include <arpa/inet.h> #include <arpa/inet.h>
#include <errno.h> #include <errno.h>
#include <netdb.h> #include <netdb.h>
@@ -190,7 +191,10 @@ bool tcp_connect_socket(Client* client, char* host, int port) {
int err = getaddrinfo(host, port_str, &hints, &result); int err = getaddrinfo(host, port_str, &hints, &result);
if (err != 0 || result == NULL) { 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 : "<allocation failed>", gai_strerror(err));
free(escaped_host);
return false; return false;
} }
+13 -3
View File
@@ -2,6 +2,7 @@
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include "transport_tcp.h" #include "transport_tcp.h"
#include "utils.h"
#include <arpa/inet.h> #include <arpa/inet.h>
#include <openssl/err.h> #include <openssl/err.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
@@ -65,13 +66,19 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
return NULL; return NULL;
} }
if (SSL_CTX_use_certificate_file(ctx, cert, SSL_FILETYPE_PEM) <= 0) { 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 : "<allocation failed>");
free(escaped);
log_ssl_errors(); log_ssl_errors();
SSL_CTX_free(ctx); SSL_CTX_free(ctx);
return NULL; return NULL;
} }
if (SSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM) <= 0) { 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 : "<allocation failed>");
free(escaped);
log_ssl_errors(); log_ssl_errors();
SSL_CTX_free(ctx); SSL_CTX_free(ctx);
return NULL; 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 (ca_path) {
if (!SSL_CTX_load_verify_locations(ctx, ca_path, NULL)) { 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 : "<allocation failed>");
free(escaped);
log_ssl_errors(); log_ssl_errors();
SSL_CTX_free(ctx); SSL_CTX_free(ctx);
return NULL; return NULL;
+30 -1
View File
@@ -1,6 +1,7 @@
#include "utils.h" #include "utils.h"
#include "array_list.h" #include "array_list.h"
#include "libgen.h" #include "libgen.h"
#include "log.h"
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
@@ -132,6 +133,32 @@ char* str_dup(const char* string) {
return new_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: /* Match a glob pattern against a string. Supported wildcards:
* ? matches any single character except '/'. * ? matches any single character except '/'.
* * matches any sequence of characters within one path component (no '/'). * * matches any sequence of characters within one path component (no '/').
@@ -293,7 +320,9 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
} else { } else {
(*deleted_count)++; (*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 : "<allocation failed>");
free(escaped_path);
} }
} }
free(child_rel); free(child_rel);
+1
View File
@@ -7,6 +7,7 @@
bool mkdir_r(const char* path); bool mkdir_r(const char* path);
char* str_dup(const char* string); 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); char* path_cat(const char* path1, const char* path2);
bool glob_match(const char* pattern, const char* str); bool glob_match(const char* pattern, const char* str);
bool delete_extras(const char* dest_root, ArrayList* manifest); bool delete_extras(const char* dest_root, ArrayList* manifest);
+19
View File
@@ -397,6 +397,24 @@ static void test_parse_args_fsync() {
config_delete(cfg); 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() { void test_client_cli() {
test_validate_config_required_paths(); test_validate_config_required_paths();
test_validate_config_incompatible_options(); test_validate_config_incompatible_options();
@@ -423,4 +441,5 @@ void test_client_cli() {
test_parse_args_human_readable(); test_parse_args_human_readable();
test_parse_args_archive(); test_parse_args_archive();
test_parse_args_fsync(); test_parse_args_fsync();
test_parse_args_8_bit_output();
} }
+2
View File
@@ -127,6 +127,7 @@ static void test_config_send_receive() {
send_cfg->use_fsync = true; send_cfg->use_fsync = true;
send_cfg->compression_level = 5; send_cfg->compression_level = 5;
send_cfg->chunk_size = 1024; send_cfg->chunk_size = 1024;
send_cfg->eight_bit_output = true;
/* Use socketpair for bidirectional communication */ /* Use socketpair for bidirectional communication */
int p[2]; int p[2];
@@ -163,6 +164,7 @@ static void test_config_send_receive() {
ok = false; ok = false;
if (!recv_cfg->use_fsync) if (!recv_cfg->use_fsync)
ok = false; ok = false;
if (!recv_cfg->eight_bit_output)
} }
config_delete(recv_cfg); config_delete(recv_cfg);
close(p[0]); close(p[0]);
+47
View File
@@ -1,8 +1,27 @@
#include "test_shared_utils.h" #include "test_shared_utils.h"
#include "utils.h" #include "utils.h"
#include "protocol.h"
#include "test_utils.h" #include "test_utils.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <threads.h>
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() { void test_shared_utils() {
char formatted[32]; char formatted[32];
@@ -14,6 +33,34 @@ void test_shared_utils() {
EXPECT_EQ_STR(formatted, "1.5 MB"); EXPECT_EQ_STR(formatted, "1.5 MB");
EXPECT_FALSE(format_human_bytes(1024, formatted, 4)); EXPECT_FALSE(format_human_bytes(1024, formatted, 4));
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);
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 // Test str_dup
const char* dup_null = str_dup(NULL); const char* dup_null = str_dup(NULL);
EXPECT_NULL(dup_null); EXPECT_NULL(dup_null);