feat: add 8-bit output option
CI / lint (pull_request) Successful in 11s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 16s
CI / coverage (pull_request) Successful in 33s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s

This commit is contained in:
2026-09-03 16:03:19 +02:00
parent 190fc5d300
commit eb4e9fba1f
13 changed files with 74 additions and 6 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
+4
View File
@@ -153,6 +153,7 @@ static const OptionEntry OPTION_TABLE[] = {
{"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)}, {"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)},
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)}, {"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
{"--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)},
@@ -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. */ /* 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 parse_args(Config* config, int argc, char* argv[], int* positional_args,
int* positional_count) { int* positional_count) {
log_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) {
@@ -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) { } else if (apply_table_option(config, entry, NULL) != 0) {
return -1; return -1;
} }
if (entry->offset == offsetof(Config, eight_bit_output))
log_set_8_bit_output(true);
continue; continue;
} }
+8 -1
View File
@@ -137,7 +137,14 @@ static int send_dry_run_manifest(const Config* config) {
printf("Dry run: files to be transferred\n"); printf("Dry run: files to be transferred\n");
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++) {
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; total_bytes += chunk->items[i]->data->size;
file_count++; file_count++;
} }
+1
View File
@@ -20,6 +20,7 @@ void print_usage(void) {
printf(" -n, --dry-run Show what would be transferred\n"); printf(" -n, --dry-run Show what would be transferred\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");
+1
View File
@@ -68,6 +68,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->append = false; config->append = false;
+1
View File
@@ -74,6 +74,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;
+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 bool eight_bit_output = false;
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
+7 -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>
@@ -328,7 +329,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, log_get_8_bit_output());
log_message(LOG_LEVEL_DEBUG, "Send String: %s", escaped ? escaped : "<allocation failed>");
free(escaped);
return true; return true;
} }
@@ -356,7 +359,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, log_get_8_bit_output());
log_message(LOG_LEVEL_DEBUG, "Received String: %s", escaped ? escaped : "<allocation failed>");
free(escaped);
return data; return data;
} }
+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 '/').
@@ -274,7 +301,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);
-1
View File
@@ -281,7 +281,6 @@ static void test_parse_args_rejects_unimplemented_options() {
"--debug", "--debug",
"--list-only", "--list-only",
"-h", "-h",
"--human-readable",
"-u", "-u",
"--update", "--update",
"--append", "--append",
+8
View File
@@ -5,6 +5,14 @@
#include <string.h> #include <string.h>
void test_shared_utils() { 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 // 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);