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:
+1
-1
@@ -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
|
||||
|
||||
+22
-5
@@ -156,6 +156,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
|
||||
{"--fsync", NULL, OPT_FLAG, offsetof(Config, use_fsync)},
|
||||
{"--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)},
|
||||
@@ -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* positional_count) {
|
||||
bool verbose = false;
|
||||
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) {
|
||||
@@ -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) {
|
||||
return -1;
|
||||
}
|
||||
if (entry->offset == offsetof(Config, eight_bit_output))
|
||||
protocol_set_8_bit_output(true);
|
||||
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");
|
||||
} 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 : "<allocation failed>");
|
||||
free(escaped);
|
||||
return -1;
|
||||
}
|
||||
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");
|
||||
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;
|
||||
}
|
||||
config->log_file = lf;
|
||||
@@ -368,14 +378,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 : "<allocation failed>");
|
||||
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 : "<allocation failed>");
|
||||
free(escaped);
|
||||
print_usage();
|
||||
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) {
|
||||
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 : "<allocation failed>", strerror(errno));
|
||||
free(escaped);
|
||||
return -1;
|
||||
}
|
||||
char* line = NULL;
|
||||
|
||||
@@ -233,11 +233,18 @@ static int send_dry_run_manifest(const Config* config) {
|
||||
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
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)
|
||||
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)));
|
||||
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;
|
||||
file_count++;
|
||||
|
||||
@@ -21,6 +21,7 @@ void print_usage(void) {
|
||||
printf(" --remove-source-files Remove regular source files after successful transfer\n");
|
||||
printf(" -p <port> 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 <pattern> Exclude files matching pattern\n");
|
||||
printf(" --include <pattern> Only include files matching pattern\n");
|
||||
|
||||
+12
-3
@@ -232,6 +232,7 @@ void handler(int file_descriptor) {
|
||||
protocol_session_unbind();
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -400,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 : "<allocation failed>");
|
||||
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 : "<allocation failed>");
|
||||
free(escaped);
|
||||
print_server_usage();
|
||||
return 1;
|
||||
}
|
||||
@@ -415,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 : "<allocation failed>");
|
||||
free(escaped);
|
||||
return 1;
|
||||
}
|
||||
if (stdio_mode) {
|
||||
|
||||
+20
-9
@@ -69,6 +69,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->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->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->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 &&
|
||||
@@ -221,11 +222,13 @@ 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) &&
|
||||
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) &&
|
||||
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) {
|
||||
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->receive_root_directory = receive_str(fd);
|
||||
if (!c->send_directory || !c->receive_root_directory)
|
||||
@@ -362,8 +368,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 : "<allocation failed>", PROTOCOL_VERSION);
|
||||
free(escaped_version);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
goto error;
|
||||
}
|
||||
@@ -375,7 +383,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 : "<allocation failed>");
|
||||
free(escaped_choice);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
goto error;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ typedef struct Config {
|
||||
int debug_level;
|
||||
bool list_only;
|
||||
bool human_readable;
|
||||
bool eight_bit_output;
|
||||
|
||||
// Issue #127: Transfer modes
|
||||
bool update;
|
||||
|
||||
@@ -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 : "<allocation failed>");
|
||||
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 : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
free(path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -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 _Thread_local bool eight_bit_output;
|
||||
|
||||
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,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define LOG_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
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
|
||||
|
||||
+13
-4
@@ -2,6 +2,7 @@
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
@@ -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 : "<allocation failed>", 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 : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
}
|
||||
|
||||
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata) {
|
||||
|
||||
+22
-2
@@ -1,5 +1,6 @@
|
||||
#include "protocol.h"
|
||||
#include "log.h"
|
||||
#include "utils.h"
|
||||
#include <errno.h>
|
||||
#include <limits.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.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());
|
||||
}
|
||||
@@ -59,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) {
|
||||
@@ -103,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;
|
||||
@@ -328,7 +344,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, session->eight_bit_output);
|
||||
log_message(LOG_LEVEL_DEBUG, "Send String: %s", escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -356,7 +374,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, session->eight_bit_output);
|
||||
log_message(LOG_LEVEL_DEBUG, "Received String: %s", escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 : "<allocation failed>");
|
||||
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 : "<allocation failed>");
|
||||
free(escaped);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "transport_tcp.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "transport_tcp.h"
|
||||
#include "utils.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <openssl/err.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;
|
||||
}
|
||||
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();
|
||||
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 : "<allocation failed>");
|
||||
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 : "<allocation failed>");
|
||||
free(escaped);
|
||||
log_ssl_errors();
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
|
||||
+30
-1
@@ -1,6 +1,7 @@
|
||||
#include "utils.h"
|
||||
#include "array_list.h"
|
||||
#include "libgen.h"
|
||||
#include "log.h"
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -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 '/').
|
||||
@@ -293,7 +320,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 : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
}
|
||||
}
|
||||
free(child_rel);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -397,6 +397,24 @@ static void test_parse_args_fsync() {
|
||||
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();
|
||||
@@ -423,4 +441,5 @@ void test_client_cli() {
|
||||
test_parse_args_human_readable();
|
||||
test_parse_args_archive();
|
||||
test_parse_args_fsync();
|
||||
test_parse_args_8_bit_output();
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ static void test_config_send_receive() {
|
||||
send_cfg->use_fsync = 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];
|
||||
@@ -163,6 +164,7 @@ static void test_config_send_receive() {
|
||||
ok = false;
|
||||
if (!recv_cfg->use_fsync)
|
||||
ok = false;
|
||||
if (!recv_cfg->eight_bit_output)
|
||||
}
|
||||
config_delete(recv_cfg);
|
||||
close(p[0]);
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
#include "test_shared_utils.h"
|
||||
#include "utils.h"
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.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() {
|
||||
char formatted[32];
|
||||
@@ -14,6 +33,34 @@ void test_shared_utils() {
|
||||
EXPECT_EQ_STR(formatted, "1.5 MB");
|
||||
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
|
||||
const char* dup_null = str_dup(NULL);
|
||||
EXPECT_NULL(dup_null);
|
||||
|
||||
Reference in New Issue
Block a user