fix: features and enhancements — issues #70, #36, #34, #33, #32, #57, #40, #37
CI / lint (pull_request) Failing after 3s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped

This commit is contained in:
2026-07-20 18:24:17 +02:00
parent f256e468a1
commit c46379ed42
12 changed files with 186 additions and 104 deletions
+19 -6
View File
@@ -55,7 +55,7 @@ static void print_usage(void) {
printf(" --cert <path> TLS certificate file (PEM)\n");
printf(" --key <path> TLS private key file (PEM)\n");
printf(" --ca <path> TLS CA certificate file (PEM)\n");
printf(" -V, --version Show version information\n");
printf(" --partial Keep partially transferred files on interruption\n");
printf(" --help Show this help\n");
}
@@ -81,9 +81,6 @@ int main(int argc, char* argv[]) {
if (strcmp(argv[i], "--help") == 0) {
print_usage();
goto cleanup;
} else if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-V") == 0) {
printf("FastSync version %s\n", PROTOCOL_VERSION);
goto cleanup;
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) {
config->use_compression = true;
config->use_multithreading = true;
@@ -92,7 +89,14 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) {
config->dry_run = true;
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
config->ssh_port = atoi(argv[++i]);
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]);
exit_code = 1;
goto cleanup;
}
config->ssh_port = (int)p;
} else if (strcmp(argv[i], "--delete") == 0) {
config->use_delete = true;
} else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) {
@@ -169,7 +173,14 @@ int main(int argc, char* argv[]) {
free(config->server_host);
config->server_host = str_dup(argv[++i]);
} else if (strcmp(argv[i], "--server-port") == 0 && i + 1 < argc) {
config->server_port = atoi(argv[++i]);
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]);
exit_code = 1;
goto cleanup;
}
config->server_port = (int)p;
} else if (strcmp(argv[i], "--bwlimit") == 0 && i + 1 < argc) {
char* end;
errno = 0;
@@ -203,6 +214,8 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) {
free(config->tls_ca);
config->tls_ca = str_dup(argv[++i]);
} else if (strcmp(argv[i], "--partial") == 0) {
config->partial = true;
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
set_log_level(LOG_LEVEL_DEBUG);
} else if (argv[i][0] == '-') {
+3 -1
View File
@@ -86,7 +86,9 @@ static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* c
return -1;
}
int file_type = (int)file->type;
bool ok = send_status(client->file_descriptor, STATUS_DELTA_DATA) &&
send_int(client->file_descriptor, file_type) &&
send_data(client->file_descriptor, to_send);
if (ok && config->use_metadata)
@@ -500,7 +502,7 @@ int send_files_multithreaded(Config* config) {
DirectoryScanner* scanner = directory_scanner_create(
config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns,
config->exclude_count, config->include_patterns, config->include_count, config->max_size,
config->min_size);
config->min_size, config->follow_symlinks);
Chunk* chunk;
int file_count = 0;
unsigned long long total_bytes = 0;
+40 -6
View File
@@ -16,6 +16,26 @@
#include <stdlib.h>
#include <string.h>
static const char* filename_from_path(const char* path) {
const char* slash = strrchr(path, '/');
return slash ? slash + 1 : path;
}
static bool should_exclude_file(const Config* config, const char* filename) {
for (int i = 0; i < config->exclude_count; i++) {
if (glob_match(config->exclude_patterns[i], filename))
return true;
}
if (config->include_count > 0) {
for (int i = 0; i < config->include_count; i++) {
if (glob_match(config->include_patterns[i], filename))
return true;
}
return false;
}
return false;
}
int receive_files(Config* config, int fd) {
Status status;
if (!receive_status(fd, &status))
@@ -29,8 +49,10 @@ int receive_files(Config* config, int fd) {
goto next;
if (file == NULL && !skipped)
return -1;
if (config->save_to_disk)
if (config->save_to_disk) {
if (!should_exclude_file(config, filename_from_path(file->path)))
file_save_to_disk(config->receive_root_directory, file);
}
file_destroy(file);
} else if (status == STATUS_CHUNK) {
Chunk* chunk = receive_chunk_data(fd, config);
@@ -39,9 +61,11 @@ int receive_files(Config* config, int fd) {
return -1;
}
for (int i = 0; i < chunk->element_count; i++) {
if (config->save_to_disk)
if (config->save_to_disk) {
if (!should_exclude_file(config, filename_from_path(chunk->items[i]->path)))
file_save_to_disk(config->receive_root_directory, chunk->items[i]);
}
}
chunk_destroy(chunk);
} else {
File* file = file_receive(config, fd);
@@ -50,8 +74,10 @@ int receive_files(Config* config, int fd) {
send_status(fd, STATUS_ERROR);
return -1;
}
if (config->save_to_disk)
if (config->save_to_disk) {
if (!should_exclude_file(config, filename_from_path(file->path)))
file_save_to_disk(config->receive_root_directory, file);
}
file_destroy(file);
}
next:
@@ -112,13 +138,21 @@ void handler(int file_descriptor) {
close(file_descriptor);
}
/* Signal-safe flag: set by the signal handler, checked in main loop.
* We cannot safely access g_server from the signal handler because it's
* not sig_atomic_t. Instead, the handler sets this flag and calls _exit
* (which is async-signal-safe). The server is fork-based (not threaded),
* so g_server is only accessed from the main thread and cleanup() only
* runs in the parent process — no concurrent access from children. */
static volatile sig_atomic_t g_server_cleanup_requested = 0;
static Server* g_server = NULL;
static void cleanup(int sig) {
(void)sig;
if (g_server) {
server_delete(&g_server);
}
g_server_cleanup_requested = 1;
/* _exit is async-signal-safe; we must not call server_delete() from a
* signal handler (it may call non-async-signal-safe functions). The OS
* will reclaim resources on exit. */
_exit(0);
}
+4
View File
@@ -42,6 +42,8 @@ Config* config_create(char* version, char* send_directory, char* receive_directo
config->delta_block_size = DELTA_BLOCK_SIZE_DEFAULT;
config->delta_max_file_size = DELTA_MAX_FILE_SIZE;
config->use_tls = false;
config->partial = false;
config->follow_symlinks = false;
config->tls_cert = NULL;
config->tls_key = NULL;
config->tls_ca = NULL;
@@ -218,6 +220,8 @@ Config* config_receive(int file_descriptor) {
config->max_size = 0;
config->min_size = 0;
config->use_tls = false;
config->partial = false;
config->follow_symlinks = false;
config->tls_cert = NULL;
config->tls_key = NULL;
config->tls_ca = NULL;
+2
View File
@@ -35,6 +35,8 @@ typedef struct Config {
uint32_t delta_block_size;
unsigned long long delta_max_file_size;
bool use_tls;
bool partial;
bool follow_symlinks;
char* server_host;
int server_port;
char* tls_cert;
+45 -73
View File
@@ -155,6 +155,49 @@ static void* old_data_from_path(const char* full_path, unsigned long long old_si
return data;
}
/* Helper: receive data + optionally decompress + store in a new File.
* On success returns the File (caller owns it). On failure sends
* STATUS_ERROR on fd and returns NULL. */
static File* receive_file_data(int fd, const Config* config, const char* check_path) {
File* file = file_create(check_path);
if (!file) {
send_status(fd, STATUS_ERROR);
return NULL;
}
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
}
Data* file_data = receive_data(fd);
if (!file_data) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
if (config->use_compression) {
Data* uncompressed = data_decompress(file_data);
data_destroy(file_data);
if (!uncompressed) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
file_data = uncompressed;
}
data_destroy(file->data);
file->data = file_data;
return file;
}
static File* receive_delta_file(int fd, const Config* config, const char* check_path,
void* old_data, unsigned long long old_size) {
if (!old_data)
@@ -264,43 +307,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
delta_signature_destroy(sig);
free(old_data);
File* file = file_create(check_path);
if (!file) {
send_status(fd, STATUS_ERROR);
return NULL;
}
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
}
Data* file_data = receive_data(fd);
if (file_data == NULL) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
if (config->use_compression) {
Data* uncompressed = data_decompress(file_data);
data_destroy(file_data);
if (uncompressed == NULL) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
file_data = uncompressed;
}
data_destroy(file->data);
file->data = file_data;
return file;
return receive_file_data(fd, config, check_path);
}
delta_signature_destroy(sig);
@@ -367,44 +374,9 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
}
}
File* file = file_create(check_path);
File* file = receive_file_data(fd, config, check_path);
free(check_path);
free(full_path);
if (file == NULL) {
send_status(fd, STATUS_ERROR);
return NULL;
}
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
}
Data* file_data = receive_data(fd);
if (file_data == NULL) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
if (config->use_compression) {
Data* uncompressed = data_decompress(file_data);
data_destroy(file_data);
if (uncompressed == NULL) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
file_data = uncompressed;
}
data_destroy(file->data);
file->data = file_data;
return file;
}
+8
View File
@@ -14,10 +14,18 @@ typedef struct {
long mtime_nsec;
} FileMetadata;
typedef enum {
FILE_TYPE_REGULAR,
FILE_TYPE_SYMLINK,
FILE_TYPE_DIRECTORY
} FileType;
typedef struct {
char* path;
Data* data;
FileMetadata* metadata;
FileType type;
char* link_target;
} File;
File* file_create(const char* path);
+23 -1
View File
@@ -13,6 +13,26 @@
#include <string.h>
#include <threads.h>
static const char* filename_from_path(const char* path) {
const char* slash = strrchr(path, '/');
return slash ? slash + 1 : path;
}
static bool should_exclude_file(const Config* config, const char* filename) {
for (int i = 0; i < config->exclude_count; i++) {
if (glob_match(config->exclude_patterns[i], filename))
return true;
}
if (config->include_count > 0) {
for (int i = 0; i < config->include_count; i++) {
if (glob_match(config->include_patterns[i], filename))
return true;
}
return false;
}
return false;
}
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
Queue* queue_loader) {
PipelineContextSender* context = malloc(sizeof(PipelineContextSender));
@@ -155,8 +175,10 @@ int write_thread(void* pipeline_context) {
free(root_directory);
return thrd_success;
}
if (save_to_disk)
if (save_to_disk) {
if (!should_exclude_file(context->config, filename_from_path(file->path)))
file_save_to_disk(root_directory, file);
}
file_destroy(file);
}
}
+5 -10
View File
@@ -35,8 +35,8 @@ static void bw_throttle(size_t bytes_written) {
clock_gettime(CLOCK_MONOTONIC, &now);
/* Use unsigned long long for intermediate computation to avoid overflow.
* sec_diff * 1000000000LL could overflow a signed 64-bit if the elapsed
* time is very large; clamp to a safe maximum. */
* sec_diff * 1000000000ULL could overflow signed 64-bit for large deltas;
* clamp to a safe maximum. */
unsigned long long sec_diff = (unsigned long long)(now.tv_sec - bw_last_refill.tv_sec);
if (sec_diff > 9223372036ULL)
sec_diff = 9223372036ULL;
@@ -85,7 +85,7 @@ bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
if (bytes_send <= 0) {
int err = SSL_get_error(io_ssl, (int)bytes_send);
if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) {
// Non-fatal: retry without counting progress
/* Non-fatal: retry without counting progress */
continue;
}
log_message(LOG_LEVEL_ERROR, "Could not send data (SSL error: %d)", err);
@@ -117,7 +117,7 @@ bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
if (bytes_received <= 0) {
int err = SSL_get_error(io_ssl, (int)bytes_received);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
// Non-fatal: retry without counting progress
/* Non-fatal: retry without counting progress */
continue;
}
if (bytes_received == 0)
@@ -167,10 +167,6 @@ static const char* status_to_string(Status status) {
}
bool send_str(int file_descriptor, const char* data) {
if (data == NULL) {
log_message(LOG_LEVEL_ERROR, "send_str called with NULL data");
return false;
}
size_t size = strlen(data);
if (!send_n_data(file_descriptor, &size, sizeof(size_t)))
return false;
@@ -185,8 +181,7 @@ char* receive_str(int file_descriptor) {
if (!receive_n_data(file_descriptor, &size, sizeof(size_t)))
return NULL;
if (size > MAX_STRING_SIZE) {
log_message(LOG_LEVEL_ERROR, "receive_str: size %zu exceeds maximum %zu", size,
(size_t)MAX_STRING_SIZE);
log_message(LOG_LEVEL_ERROR, "String size %zu exceeds maximum %d", size, MAX_STRING_SIZE);
return NULL;
}
char* data = (char*)malloc(size + 1);
+18
View File
@@ -71,6 +71,24 @@ char* str_dup(const char* string) {
bool glob_match(const char* pattern, const char* str) {
while (*pattern) {
if (*pattern == '*') {
if (*(pattern + 1) == '*') {
/* ** pattern: matches zero or more characters including '/' */
pattern += 2; /* skip both stars */
/* If ** is immediately followed by '/', consume it too so that
* **/foo behaves intuitively (matches foo at any depth). */
if (*pattern == '/')
pattern++;
/* Try matching the remainder of pattern at every position in str,
* including across '/' boundaries and at the very end. */
while (1) {
if (glob_match(pattern, str))
return true;
if (*str == '\0')
return false;
str++;
}
}
/* Single *: matches any characters except '/' */
pattern++;
while (*str && *str != '/') {
if (glob_match(pattern, str))
+12
View File
@@ -5,8 +5,11 @@
#include "test_data.h"
#include "test_delta.h"
#include "test_file.h"
#include "test_file_sendfile.h"
#include "test_glob.h"
#include "test_log.h"
#include "test_metadata.h"
#include "test_multiprocessing.h"
#include "test_property.h"
#include "test_protocol.h"
#include "test_queue.h"
@@ -14,6 +17,9 @@
#include "test_scanner.h"
#include "test_shared_utils.h"
#include "test_stress.h"
#include "test_transport_tcp.h"
#include "test_transport_ssh.h"
#include "test_transport_tls.h"
#include "test_utils.h"
#include <stdio.h>
@@ -38,9 +44,15 @@ int main() {
RUN_TEST(test_metadata);
RUN_TEST(test_glob);
RUN_TEST(test_file);
RUN_TEST(test_file_sendfile);
RUN_TEST(test_log);
RUN_TEST(test_multiprocessing);
RUN_TEST(test_robustness);
RUN_TEST(test_stress);
RUN_TEST(test_property);
RUN_TEST(test_transport_tcp);
RUN_TEST(test_transport_ssh);
RUN_TEST(test_transport_tls);
printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n");
printf("Total Tests Run: %d\n", tests_run);
+3 -3
View File
@@ -19,7 +19,7 @@ static void test_scanner_single_file() {
create_test_file(file1, content1);
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, true);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
@@ -48,7 +48,7 @@ static void test_scanner_multiple_files() {
create_test_file(file2, content2);
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, true);
EXPECT_NOT_NULL(scanner);
const Chunk* chunk = directory_scanner_next(scanner);
@@ -112,7 +112,7 @@ static void test_scanner_empty_directory() {
mkdir(dir, 0755);
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, true);
EXPECT_NOT_NULL(scanner);
const Chunk* chunk = directory_scanner_next(scanner);