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
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:
+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 |
|
| `--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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user