From 4f383cc5a596fcf0cc61f49f0368b9f766ce2c32 Mon Sep 17 00:00:00 2001 From: TapTap Date: Wed, 29 Jul 2026 19:02:09 +0200 Subject: [PATCH] fix: resolve all bug issues (#158, #163, #164, #165, #166, #167, #168, #169) --- src/client/client_cli.c | 653 +++++++++++++++++++++-------------- src/client/client_send.c | 110 +++--- src/server/server.c | 6 +- src/shared/config.c | 30 +- src/shared/config.h | 5 +- src/shared/file.c | 2 + src/shared/multiprocessing.c | 9 +- src/shared/protocol.c | 2 +- tests/runner.c | 6 + tests/test_client_cli.c | 89 +++++ tests/test_client_cli.h | 6 + tests/test_config.c | 177 +++++++++- tests/test_file.c | 169 ++++++++- tests/test_file_sendfile.c | 21 +- tests/test_fuzz_smoke.c | 176 ++++++++++ tests/test_fuzz_smoke.h | 6 + tests/test_multiprocessing.c | 140 +++++++- tests/test_server.c | 174 ++++++++++ tests/test_server.h | 6 + tests/test_transport_tcp.c | 59 ++++ tests/test_transport_tls.c | 37 ++ 21 files changed, 1498 insertions(+), 385 deletions(-) create mode 100644 tests/test_client_cli.c create mode 100644 tests/test_client_cli.h create mode 100644 tests/test_fuzz_smoke.c create mode 100644 tests/test_fuzz_smoke.h create mode 100644 tests/test_server.c create mode 100644 tests/test_server.h diff --git a/src/client/client_cli.c b/src/client/client_cli.c index ba17a21..67d591f 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -12,6 +12,341 @@ #include #include +/* Parse environment variables for source/destination directories and save-to-disk flag. */ +static void parse_environment(const char** out_env_source, const char** out_env_dest, + bool* out_save_to_disk) { + *out_env_source = getenv("FASTSYNC_SOURCE_DIR"); + *out_env_dest = getenv("FASTSYNC_DEST_DIR"); + const char* env_save = getenv("FASTSYNC_SAVE_TO_DISK"); + *out_save_to_disk = false; + if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) + *out_save_to_disk = true; +} + +/* Parse a string as a positive integer, returning true on success. */ +static bool parse_positive_int(const char* s, int* out_val) { + if (!s || *s == '\0') + return false; + char* endptr; + errno = 0; + long val = strtol(s, &endptr, 10); + if (errno != 0 || *endptr != '\0' || val <= 0 || val > INT_MAX) + return false; + *out_val = (int)val; + return true; +} + +/* Parse a string as a non-negative integer, returning true on success. */ +static bool parse_nonneg_int(const char* s, int* out_val) { + if (!s || *s == '\0') + return false; + char* endptr; + errno = 0; + long val = strtol(s, &endptr, 10); + if (errno != 0 || *endptr != '\0' || val < 0 || val > INT_MAX) + return false; + *out_val = (int)val; + return true; +} + +static void print_usage(void); +static int read_patterns_from_file(const char* filepath, char*** patterns, int* count); + +/* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */ +static int parse_args(Config* config, int argc, char* argv[], int* positional_args, + int* positional_count) { + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0) { + print_usage(); + return 1; + } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) { + config->use_compression = true; + config->use_multithreading = true; + config->use_metadata = true; + log_message(LOG_LEVEL_INFO, "Enabled archive mode (-c -m -M)"); + } 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) { + if (!parse_positive_int(argv[++i], &config->ssh_port)) { + fprintf(stderr, "Error: invalid --port/-p value: %s\n", argv[i]); + return -1; + } + } else if (strcmp(argv[i], "--delete") == 0) { + config->use_delete = true; + } else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) { + char** tmp = realloc(config->exclude_patterns, (config->exclude_count + 1) * sizeof(char*)); + if (!tmp) { + fprintf(stderr, "Error: memory allocation failed for --exclude\n"); + return -1; + } + config->exclude_patterns = tmp; + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --exclude\n"); + return -1; + } + config->exclude_patterns[config->exclude_count++] = dup; + } else if (strcmp(argv[i], "--include") == 0 && i + 1 < argc) { + char** tmp = realloc(config->include_patterns, (config->include_count + 1) * sizeof(char*)); + if (!tmp) { + fprintf(stderr, "Error: memory allocation failed for --include\n"); + return -1; + } + config->include_patterns = tmp; + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --include\n"); + return -1; + } + config->include_patterns[config->include_count++] = dup; + } else if (strcmp(argv[i], "--max-size") == 0 && i + 1 < argc) { + config->max_size = strtoull(argv[++i], NULL, 10); + } else if (strcmp(argv[i], "--min-size") == 0 && i + 1 < argc) { + config->min_size = strtoull(argv[++i], NULL, 10); + } else if (strcmp(argv[i], "--incremental") == 0) { + config->use_incremental = true; + } else if (strcmp(argv[i], "--delta") == 0) { + config->use_delta = true; + } else if (strcmp(argv[i], "--delta-block") == 0 && i + 1 < argc) { + unsigned long long val = strtoull(argv[++i], NULL, 10); + if (val >= DELTA_BLOCK_SIZE_MIN && val <= DELTA_BLOCK_SIZE_MAX) + config->delta_block_size = (uint32_t)val; + else + fprintf(stderr, "Warning: --delta-block value %llu out of range, using default\n", val); + } else if (strcmp(argv[i], "--delta-max") == 0 && i + 1 < argc) { + unsigned long long val = strtoull(argv[++i], NULL, 10); + if (val >= DELTA_MIN_FILE_SIZE) + config->delta_max_file_size = val; + else + fprintf(stderr, "Warning: --delta-max value %llu too small, using default\n", val); + } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "-z") == 0) { + config->use_compression = true; + log_message(LOG_LEVEL_INFO, "Enabled Compression"); + if (i + 1 < argc) { + char* end_ptr; + long level = strtol(argv[i + 1], &end_ptr, 10); + if (*end_ptr == '\0') { + config->compression_level = (int)level; + log_message(LOG_LEVEL_INFO, "Set Compression level to %ld", level); + i++; + } + } + } else if (strcmp(argv[i], "--source-dir") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --source-dir\n"); + return -1; + } + free(config->send_directory); + config->send_directory = dup; + } else if (strcmp(argv[i], "--dest-dir") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --dest-dir\n"); + return -1; + } + free(config->receive_root_directory); + config->receive_root_directory = dup; + } else if (strcmp(argv[i], "--save-to-disk") == 0) { + config->save_to_disk = true; + } else if (strcmp(argv[i], "-M") == 0 || strcmp(argv[i], "--preserve") == 0) { + config->use_metadata = true; + log_message(LOG_LEVEL_INFO, "Enabled metadata preservation"); + } else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--sendfile") == 0) { + config->use_sendfile = true; + log_message(LOG_LEVEL_INFO, "Enabled sendfile"); + } else if (strcmp(argv[i], "-m") == 0) { + config->use_multithreading = true; + log_message(LOG_LEVEL_INFO, "Enabled Multithreading"); + } else if (strcmp(argv[i], "-s") == 0) { + config->use_chunk_serialization = true; + log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization"); + } else if (strcmp(argv[i], "--server-host") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --server-host\n"); + return -1; + } + free(config->server_host); + config->server_host = dup; + } else if (strcmp(argv[i], "--server-port") == 0 && i + 1 < argc) { + if (!parse_positive_int(argv[++i], &config->server_port)) { + fprintf(stderr, "Error: invalid --server-port value: %s\n", argv[i]); + return -1; + } + } else if (strcmp(argv[i], "--bwlimit") == 0 && i + 1 < argc) { + char* end; + errno = 0; + unsigned long long kbps = strtoull(argv[++i], &end, 10); + if (errno != 0 || *end != '\0' || kbps == 0) { + fprintf(stderr, "Error: --bwlimit must be a positive integer\n"); + return -1; + } + if (kbps > ULLONG_MAX / 1024) { + fprintf(stderr, "Error: --bwlimit value too large\n"); + return -1; + } + io_set_bwlimit(kbps * 1024); + log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps); + } else if (strcmp(argv[i], "--progress") == 0) { + config->show_progress = true; + } else if (strcmp(argv[i], "--chunk-size") == 0 && i + 1 < argc) { + unsigned long long val = strtoull(argv[++i], NULL, 10); + if (val > 0) + config->chunk_size = val; + } else if (strcmp(argv[i], "--tls") == 0) { + config->use_tls = true; + } else if (strcmp(argv[i], "--cert") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --cert\n"); + return -1; + } + free(config->tls_cert); + config->tls_cert = dup; + } else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --key\n"); + return -1; + } + free(config->tls_key); + config->tls_key = dup; + } else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --ca\n"); + return -1; + } + free(config->tls_ca); + config->tls_ca = dup; + } else if (strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) { + int val; + if (!parse_positive_int(argv[++i], &val)) { + fprintf(stderr, "Error: --timeout must be a positive integer\n"); + return -1; + } + config->timeout = val; + } else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) { + int val; + if (!parse_positive_int(argv[++i], &val)) { + fprintf(stderr, "Error: --contimeout must be a positive integer\n"); + return -1; + } + config->contimeout = val; + } else if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0 || + strcmp(argv[i], "--silent") == 0) { + config->quiet = true; + } else if (strcmp(argv[i], "--backup") == 0) { + config->backup = true; + } else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --backup-dir\n"); + return -1; + } + free(config->backup_dir); + config->backup_dir = dup; + } else if (strcmp(argv[i], "--stats") == 0) { + config->stats = true; + } else if (strcmp(argv[i], "--max-depth") == 0 && i + 1 < argc) { + if (!parse_nonneg_int(argv[++i], &config->max_depth)) { + fprintf(stderr, "Error: --max-depth must be a non-negative integer\n"); + return -1; + } + } else if (strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) { + FILE* lf = fopen(argv[++i], "a"); + if (!lf) { + fprintf(stderr, "Error: could not open log file '%s': %s\n", argv[i], strerror(errno)); + return -1; + } + config->log_file = lf; + log_set_file(lf); + } else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) { + int val; + if (!parse_positive_int(argv[++i], &val)) { + fprintf(stderr, "Error: --queue-size must be a positive integer\n"); + return -1; + } + config->queue_size = val; + } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { + if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != + 0) + return -1; + } else if (strcmp(argv[i], "--include-from") == 0 && i + 1 < argc) { + if (read_patterns_from_file(argv[++i], &config->include_patterns, &config->include_count) != + 0) + return -1; + } else if (strcmp(argv[i], "--partial") == 0) { + config->partial = true; + } else if (strcmp(argv[i], "--fastsync-server-path") == 0 && i + 1 < argc) { + char* dup = str_dup(argv[++i]); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for --fastsync-server-path\n"); + return -1; + } + free(config->fastsync_server_path); + config->fastsync_server_path = dup; + } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { + set_log_level(LOG_LEVEL_DEBUG); + } else if (argv[i][0] == '-') { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(); + return -1; + } else { + if (*positional_count < 2) + positional_args[(*positional_count)++] = i; + else { + fprintf(stderr, "Unexpected argument: %s\n", argv[i]); + print_usage(); + return -1; + } + } + } + return 0; +} + +/* Validate config after parsing. Returns true if valid. */ +static bool validate_config(const Config* config) { + if (!config->send_directory || !config->receive_root_directory) { + fprintf(stderr, "Error: source and destination directories are required\n"); + print_usage(); + return false; + } + if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) { + fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk " + "serialization)\n"); + return false; + } + if (config->transport == TRANSPORT_SSH && config->use_sendfile) { + fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); + return false; + } + if (config->use_incremental && config->use_chunk_serialization) { + fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n"); + return false; + } + if (config->use_delta && !config->use_incremental) { + fprintf(stderr, "Error: --delta requires --incremental\n"); + return false; + } + if (config->use_delta && config->use_chunk_serialization) { + fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n"); + return false; + } + if (config->use_delta && config->use_sendfile) { + fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n"); + return false; + } + if (config->use_tls) { + if (!config->tls_cert || !config->tls_key) { + fprintf(stderr, "Error: --tls requires --cert and --key\n"); + return false; + } + } + return true; +} + static void print_usage(void) { printf("Usage:\n"); printf(" fastsync [options] \n"); @@ -99,249 +434,60 @@ static int read_patterns_from_file(const char* filepath, char*** patterns, int* return -1; } *patterns = tmp; - (*patterns)[(*count)++] = str_dup(p); + char* dup = str_dup(p); + if (!dup) { + fprintf(stderr, "Error: memory allocation failed for pattern file\n"); + fclose(fp); + return -1; + } + (*patterns)[(*count)++] = dup; } fclose(fp); return 0; } int main(int argc, char* argv[]) { - const char* env_source = getenv("FASTSYNC_SOURCE_DIR"); - const char* env_dest = getenv("FASTSYNC_DEST_DIR"); - const char* env_save = getenv("FASTSYNC_SAVE_TO_DISK"); - + const char* env_source = NULL; + const char* env_dest = NULL; bool save_to_disk = false; - if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) { - save_to_disk = true; - } + parse_environment(&env_source, &env_dest, &save_to_disk); int exit_code = 0; - Config* config = NULL; bool config_owned_by_pipeline = false; - - char* config_version = str_dup(PROTOCOL_VERSION); - if (!config_version) { - exit_code = 1; - goto cleanup; + Config* config = config_create(); + if (!config) { + fprintf(stderr, "Error: failed to allocate config\n"); + return 1; } - config = config_create(config_version, NULL, NULL, save_to_disk, false, false, false, false, 5, - false, 0); + config->save_to_disk = save_to_disk; int positional_args[2]; int positional_count = 0; - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--help") == 0) { - print_usage(); - goto cleanup; - } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) { - config->use_compression = true; - config->use_multithreading = true; - config->use_metadata = true; - log_message(LOG_LEVEL_INFO, "Enabled archive mode (-c -m -M)"); - } 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]); - } else if (strcmp(argv[i], "--delete") == 0) { - config->use_delete = true; - } else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) { - char** tmp = realloc(config->exclude_patterns, (config->exclude_count + 1) * sizeof(char*)); - if (!tmp) { - fprintf(stderr, "Error: memory allocation failed for --exclude\n"); - exit_code = 1; - goto cleanup; - } - config->exclude_patterns = tmp; - config->exclude_patterns[config->exclude_count++] = str_dup(argv[++i]); - } else if (strcmp(argv[i], "--include") == 0 && i + 1 < argc) { - char** tmp = realloc(config->include_patterns, (config->include_count + 1) * sizeof(char*)); - if (!tmp) { - fprintf(stderr, "Error: memory allocation failed for --include\n"); - exit_code = 1; - goto cleanup; - } - config->include_patterns = tmp; - config->include_patterns[config->include_count++] = str_dup(argv[++i]); - } else if (strcmp(argv[i], "--max-size") == 0 && i + 1 < argc) { - config->max_size = strtoull(argv[++i], NULL, 10); - } else if (strcmp(argv[i], "--min-size") == 0 && i + 1 < argc) { - config->min_size = strtoull(argv[++i], NULL, 10); - } else if (strcmp(argv[i], "--incremental") == 0) { - config->use_incremental = true; - } else if (strcmp(argv[i], "--delta") == 0) { - config->use_delta = true; - } else if (strcmp(argv[i], "--delta-block") == 0 && i + 1 < argc) { - unsigned long long val = strtoull(argv[++i], NULL, 10); - if (val >= DELTA_BLOCK_SIZE_MIN && val <= DELTA_BLOCK_SIZE_MAX) - config->delta_block_size = (uint32_t)val; - else - fprintf(stderr, "Warning: --delta-block value %llu out of range, using default\n", val); - } else if (strcmp(argv[i], "--delta-max") == 0 && i + 1 < argc) { - unsigned long long val = strtoull(argv[++i], NULL, 10); - if (val >= DELTA_MIN_FILE_SIZE) - config->delta_max_file_size = val; - else - fprintf(stderr, "Warning: --delta-max value %llu too small, using default\n", val); - } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "-z") == 0) { - config->use_compression = true; - log_message(LOG_LEVEL_INFO, "Enabled Compression"); - if (i + 1 < argc) { - char* end_ptr; - int level = strtol(argv[i + 1], &end_ptr, 10); - if (*end_ptr == '\0') { - config->compression_level = level; - log_message(LOG_LEVEL_INFO, "Set Compression level to %d", config->compression_level); - i++; - } - } - } else if (strcmp(argv[i], "--source-dir") == 0 && i + 1 < argc) { - free(config->send_directory); - config->send_directory = str_dup(argv[++i]); - } else if (strcmp(argv[i], "--dest-dir") == 0 && i + 1 < argc) { - free(config->receive_root_directory); - config->receive_root_directory = str_dup(argv[++i]); - } else if (strcmp(argv[i], "--save-to-disk") == 0) { - config->save_to_disk = true; - } else if (strcmp(argv[i], "-M") == 0 || strcmp(argv[i], "--preserve") == 0) { - config->use_metadata = true; - log_message(LOG_LEVEL_INFO, "Enabled metadata preservation"); - } else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--sendfile") == 0) { - config->use_sendfile = true; - log_message(LOG_LEVEL_INFO, "Enabled sendfile"); - } else if (strcmp(argv[i], "-m") == 0) { - config->use_multithreading = true; - log_message(LOG_LEVEL_INFO, "Enabled Multithreading"); - } else if (strcmp(argv[i], "-s") == 0) { - config->use_chunk_serialization = true; - log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization"); - } else if (strcmp(argv[i], "--server-host") == 0 && i + 1 < argc) { - 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]); - } else if (strcmp(argv[i], "--bwlimit") == 0 && i + 1 < argc) { - char* end; - errno = 0; - unsigned long long kbps = strtoull(argv[++i], &end, 10); - if (errno != 0 || *end != '\0' || kbps == 0) { - fprintf(stderr, "Error: --bwlimit must be a positive integer\n"); - exit_code = 1; - goto cleanup; - } - if (kbps > ULLONG_MAX / 1024) { - fprintf(stderr, "Error: --bwlimit value too large\n"); - exit_code = 1; - goto cleanup; - } - io_set_bwlimit(kbps * 1024); - log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps); - } else if (strcmp(argv[i], "--progress") == 0) { - config->show_progress = true; - } else if (strcmp(argv[i], "--chunk-size") == 0 && i + 1 < argc) { - unsigned long long val = strtoull(argv[++i], NULL, 10); - if (val > 0) - config->chunk_size = val; - } else if (strcmp(argv[i], "--tls") == 0) { - config->use_tls = true; - } else if (strcmp(argv[i], "--cert") == 0 && i + 1 < argc) { - free(config->tls_cert); - config->tls_cert = str_dup(argv[++i]); - } else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) { - free(config->tls_key); - config->tls_key = str_dup(argv[++i]); - } 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], "--timeout") == 0 && i + 1 < argc) { - config->timeout = atoi(argv[++i]); - if (config->timeout <= 0) { - fprintf(stderr, "Error: --timeout must be a positive integer\n"); - exit_code = 1; - goto cleanup; - } - } else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) { - config->contimeout = atoi(argv[++i]); - if (config->contimeout <= 0) { - fprintf(stderr, "Error: --contimeout must be a positive integer\n"); - exit_code = 1; - goto cleanup; - } - } else if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0 || - strcmp(argv[i], "--silent") == 0) { - config->quiet = true; - } else if (strcmp(argv[i], "--backup") == 0) { - config->backup = true; - } else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) { - config->backup_dir = str_dup(argv[++i]); - } else if (strcmp(argv[i], "--stats") == 0) { - config->stats = true; - } else if (strcmp(argv[i], "--max-depth") == 0 && i + 1 < argc) { - config->max_depth = atoi(argv[++i]); - if (config->max_depth < 0) { - fprintf(stderr, "Error: --max-depth must be a non-negative integer\n"); - exit_code = 1; - goto cleanup; - } - } else if (strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) { - FILE* lf = fopen(argv[++i], "a"); - if (!lf) { - fprintf(stderr, "Error: could not open log file '%s': %s\n", argv[i], strerror(errno)); - exit_code = 1; - goto cleanup; - } - config->log_file = lf; - log_set_file(lf); - } else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) { - config->queue_size = atoi(argv[++i]); - if (config->queue_size <= 0) { - fprintf(stderr, "Error: --queue-size must be a positive integer\n"); - exit_code = 1; - goto cleanup; - } - } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { - if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != - 0) { - exit_code = 1; - goto cleanup; - } - } else if (strcmp(argv[i], "--include-from") == 0 && i + 1 < argc) { - if (read_patterns_from_file(argv[++i], &config->include_patterns, &config->include_count) != - 0) { - exit_code = 1; - goto cleanup; - } - } else if (strcmp(argv[i], "--partial") == 0) { - config->partial = true; - } else if (strcmp(argv[i], "--fastsync-server-path") == 0 && i + 1 < argc) { - free(config->fastsync_server_path); - config->fastsync_server_path = str_dup(argv[++i]); - } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { - set_log_level(LOG_LEVEL_DEBUG); - } else if (argv[i][0] == '-') { - fprintf(stderr, "Unknown option: %s\n", argv[i]); - print_usage(); + int parse_ret = parse_args(config, argc, argv, positional_args, &positional_count); + if (parse_ret != 0) { + if (parse_ret < 0) exit_code = 1; - goto cleanup; - } else { - if (positional_count < 2) - positional_args[positional_count++] = i; - else { - fprintf(stderr, "Unexpected argument: %s\n", argv[i]); - print_usage(); - exit_code = 1; - goto cleanup; - } - } + goto cleanup; } + /* Handle positional arguments or fall back to environment variables */ if (positional_count == 2) { free(config->send_directory); free(config->receive_root_directory); config->send_directory = str_dup(argv[positional_args[0]]); + if (!config->send_directory) { + fprintf(stderr, "Error: memory allocation failed\n"); + exit_code = 1; + goto cleanup; + } config->receive_root_directory = str_dup(argv[positional_args[1]]); + if (!config->receive_root_directory) { + fprintf(stderr, "Error: memory allocation failed\n"); + exit_code = 1; + goto cleanup; + } config->save_to_disk = true; - config_parse_ssh_dest(config); } else if (positional_count == 1) { fprintf(stderr, "Error: missing destination argument\n"); @@ -349,73 +495,46 @@ int main(int argc, char* argv[]) { exit_code = 1; goto cleanup; } else { - if (!config->send_directory && env_source) - config->send_directory = str_dup((char*)env_source); - if (!config->receive_root_directory && env_dest) - config->receive_root_directory = str_dup((char*)env_dest); + if (!config->send_directory && env_source) { + config->send_directory = str_dup(env_source); + if (!config->send_directory) { + fprintf(stderr, "Error: memory allocation failed\n"); + exit_code = 1; + goto cleanup; + } + } + if (!config->receive_root_directory && env_dest) { + config->receive_root_directory = str_dup(env_dest); + if (!config->receive_root_directory) { + fprintf(stderr, "Error: memory allocation failed\n"); + exit_code = 1; + goto cleanup; + } + } } - if (!config->send_directory || !config->receive_root_directory) { - fprintf(stderr, "Error: source and destination directories are required\n"); - print_usage(); - exit_code = 1; - goto cleanup; - } - if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) { - fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk " - "serialization)\n"); - exit_code = 1; - goto cleanup; - } - - if (config->transport == TRANSPORT_SSH && config->use_sendfile) { - fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); - exit_code = 1; - goto cleanup; - } - - if (config->use_incremental && config->use_chunk_serialization) { - fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n"); + if (!validate_config(config)) { exit_code = 1; goto cleanup; } + /* Enable implicit flags */ if (config->use_incremental && !config->use_metadata) { log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --incremental"); config->use_metadata = true; } - - if (config->use_delta && !config->use_incremental) { - fprintf(stderr, "Error: --delta requires --incremental\n"); - exit_code = 1; - goto cleanup; - } - if (config->use_delta && config->use_chunk_serialization) { - fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n"); - exit_code = 1; - goto cleanup; - } - if (config->use_delta && config->use_sendfile) { - fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n"); - exit_code = 1; - goto cleanup; - } if (config->use_delta && !config->use_metadata) { log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta"); config->use_metadata = true; } - if (config->use_tls) { - if (!config->tls_cert || !config->tls_key) { - fprintf(stderr, "Error: --tls requires --cert and --key\n"); - exit_code = 1; - goto cleanup; - } + /* Initialize TLS if needed */ + if (config->use_tls) tls_global_init(); - } tcp_set_timeouts(config->timeout, config->contimeout); + /* Execute transfer */ if (config->use_multithreading) { config_owned_by_pipeline = true; exit_code = send_files_multithreaded(config); diff --git a/src/client/client_send.c b/src/client/client_send.c index adc582c..6499aea 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -25,6 +25,44 @@ #define STREAM_THRESHOLD (64ULL * 1024 * 1024) +/* Print dry-run manifest showing files that would be transferred. Returns 0 on success. */ +static int send_dry_run_manifest(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->max_depth); + if (!scanner) + return -1; + Chunk* chunk; + int file_count = 0; + unsigned long long total_bytes = 0; + printf("Dry run: files to be transferred\n"); + while ((chunk = directory_scanner_next(scanner)) != NULL) { + for (int i = 0; i < chunk->element_count; i++) { + printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); + total_bytes += chunk->items[i]->data->size; + file_count++; + } + chunk_destroy(chunk); + } + directory_scanner_destroy(scanner); + printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); + return 0; +} + +/* Send the delete manifest (list of files) to the server. Returns 0 on success, -1 on failure. */ +static int send_delete_manifest(int fd, ArrayList* manifest) { + if (!send_status(fd, STATUS_MANIFEST)) + return -1; + if (!send_int(fd, manifest->size)) + return -1; + for (int i = 0; i < manifest->size; i++) { + if (!send_str(fd, (char*)manifest->items[i])) + return -1; + } + return 0; +} + static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) { *out_sig = NULL; if (!send_status(client->file_descriptor, STATUS_CHECK)) @@ -268,14 +306,8 @@ static int send_chunks_multithreaded(void* pipeline_context) { &context->condition_not_full_loader, &context->loader_done); if (current_chunk == NULL) { if (context->config->use_delete) { - if (!send_status(client->file_descriptor, STATUS_MANIFEST)) + if (send_delete_manifest(client->file_descriptor, context->manifest) != 0) goto send_fail; - if (!send_int(client->file_descriptor, context->manifest->size)) - goto send_fail; - for (int i = 0; i < context->manifest->size; i++) { - if (!send_str(client->file_descriptor, (char*)context->manifest->items[i])) - goto send_fail; - } } if (!send_status(client->file_descriptor, STATUS_FINISHED)) goto send_fail; @@ -365,27 +397,8 @@ static int load_files_multithreaded(void* pipeline_context) { } int send_files(Config* config) { - if (config->dry_run) { - 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->max_depth); - Chunk* chunk; - int file_count = 0; - unsigned long long total_bytes = 0; - printf("Dry run: files to be transferred\n"); - while ((chunk = directory_scanner_next(scanner)) != NULL) { - for (int i = 0; i < chunk->element_count; i++) { - printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); - total_bytes += chunk->items[i]->data->size; - file_count++; - } - chunk_destroy(chunk); - } - directory_scanner_destroy(scanner); - printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); - return 0; - } + if (config->dry_run) + return send_dry_run_manifest(config); Client* client; if (config->transport == TRANSPORT_SSH) { @@ -470,20 +483,10 @@ int send_files(Config* config) { chunk_destroy(current_chunk); } if (config->use_delete) { - if (!send_status(client->file_descriptor, STATUS_MANIFEST)) { + if (send_delete_manifest(client->file_descriptor, manifest) != 0) { array_list_delete(manifest); goto send_fail; } - if (!send_int(client->file_descriptor, manifest->size)) { - array_list_delete(manifest); - goto send_fail; - } - for (int i = 0; i < manifest->size; i++) { - if (!send_str(client->file_descriptor, (char*)manifest->items[i])) { - array_list_delete(manifest); - goto send_fail; - } - } array_list_delete(manifest); } if (!send_status(client->file_descriptor, STATUS_FINISHED)) @@ -498,37 +501,18 @@ int send_files(Config* config) { directory_scanner_destroy(scanner); client_disconnect(client); client_delete(client); - return ok ? 0 : -1; + return ok ? 0 : 1; send_fail: directory_scanner_destroy(scanner); client_disconnect(client); client_delete(client); - return -1; + return 1; } int send_files_multithreaded(Config* config) { - if (config->dry_run) { - 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->max_depth); - Chunk* chunk; - int file_count = 0; - unsigned long long total_bytes = 0; - printf("Dry run: files to be transferred\n"); - while ((chunk = directory_scanner_next(scanner)) != NULL) { - for (int i = 0; i < chunk->element_count; i++) { - printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); - total_bytes += chunk->items[i]->data->size; - file_count++; - } - chunk_destroy(chunk); - } - directory_scanner_destroy(scanner); - printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); - return 0; - } + if (config->dry_run) + return send_dry_run_manifest(config); long pages = sysconf(_SC_AVPHYS_PAGES); long page_size = sysconf(_SC_PAGE_SIZE); @@ -575,5 +559,5 @@ int send_files_multithreaded(Config* config) { thrd_join(sender, &sender_result); pipeline_context_sender_destroy(context); - return sender_result == thrd_success ? 0 : -1; + return sender_result == thrd_success ? 0 : 1; } diff --git a/src/server/server.c b/src/server/server.c index 2e045ca..d750db8 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -144,8 +144,10 @@ void handler(int file_descriptor) { thrd_join(writer, NULL); send_status(file_descriptor, STATUS_OK); pipeline_context_receiver_destroy(context); - } else + } else { receive_files(config, file_descriptor); + config_delete(config); + } close(file_descriptor); } @@ -174,6 +176,7 @@ static void print_server_usage(void) { printf(" --help Show this help\n"); } +#ifndef FASTSYNC_SERVER_AS_LIB int main(int argc, char* argv[]) { bool use_tls = false; char* tls_cert = NULL; @@ -244,3 +247,4 @@ int main(int argc, char* argv[]) { } return 0; } +#endif /* !FASTSYNC_SERVER_AS_LIB */ diff --git a/src/shared/config.c b/src/shared/config.c index ae8936b..0a550d4 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -8,28 +8,24 @@ #include #include -Config* config_create(char* version, char* send_directory, char* receive_directory, - bool save_to_disk, bool use_multithreading, bool use_chunk_serialization, - bool use_compression, bool use_metadata, int compression_level, - bool use_sendfile, unsigned long long chunk_size) { - +Config* config_create(void) { Config* config = malloc(sizeof(Config)); if (!config) return NULL; - config->version = version; - config->send_directory = send_directory; - config->receive_root_directory = receive_directory; - config->save_to_disk = save_to_disk; - config->use_multithreading = use_multithreading; - config->use_chunk_serialization = use_chunk_serialization; - config->use_compression = use_compression; - config->use_metadata = use_metadata; + config->version = str_dup(PROTOCOL_VERSION); + config->send_directory = NULL; + config->receive_root_directory = NULL; + config->save_to_disk = false; + config->use_multithreading = false; + config->use_chunk_serialization = false; + config->use_compression = false; + config->use_metadata = false; config->show_progress = false; config->dry_run = false; config->use_delete = false; - config->compression_level = compression_level; - config->use_sendfile = use_sendfile; - config->chunk_size = chunk_size > 0 ? chunk_size : DEFAULT_CHUNK_SIZE; + config->compression_level = 5; + config->use_sendfile = false; + config->chunk_size = DEFAULT_CHUNK_SIZE; config->ssh_port = 22; config->transport = TRANSPORT_TCP; config->ssh_destination = NULL; @@ -59,6 +55,8 @@ Config* config_create(char* version, char* send_directory, char* receive_directo config->max_depth = 0; config->log_file = NULL; config->queue_size = 100; + config->follow_symlinks = false; + config->partial = false; return config; } diff --git a/src/shared/config.h b/src/shared/config.h index 894b061..151c055 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -58,10 +58,7 @@ typedef struct Config { #define PROTOCOL_VERSION "1.3.0" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) -Config* config_create(char* version, char* send_directory, char* receive_directory, - bool save_to_disk, bool use_multithreading, bool use_chunk_serialization, - bool use_compression, bool use_metadata, int compression_level, - bool use_sendfile, unsigned long long chunk_size); +Config* config_create(void); void config_delete(Config* config); bool config_send(int file_descriptor, const Config* config); Config* config_receive(int file_descriptor); diff --git a/src/shared/file.c b/src/shared/file.c index a531e3d..dd74222 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -510,6 +510,8 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int while ((unsigned long long)offset < file_size) { ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset); if (sent == -1) { + if (errno == EAGAIN || errno == EINTR) + continue; perror("sendfile failed"); close(fd); return false; diff --git a/src/shared/multiprocessing.c b/src/shared/multiprocessing.c index 9aaa599..72699a1 100644 --- a/src/shared/multiprocessing.c +++ b/src/shared/multiprocessing.c @@ -81,10 +81,10 @@ void pipeline_context_receiver_destroy(PipelineContextReceiver* context) { free(context); } -static void receive_chunk_enqueue(int file_descriptor, PipelineContextReceiver* context) { +static bool receive_chunk_enqueue(int file_descriptor, PipelineContextReceiver* context) { Chunk* chunk = receive_chunk_data(file_descriptor, context->config); if (chunk == NULL) - return; + return false; for (int i = 0; i < chunk->element_count; i++) { File* file = chunk->items[i]; @@ -93,6 +93,7 @@ static void receive_chunk_enqueue(int file_descriptor, PipelineContextReceiver* &context->condition_not_empty, &context->condition_not_full); } chunk_destroy(chunk); + return true; } int receive_thread(void* pipeline_context) { @@ -125,7 +126,8 @@ int receive_thread(void* pipeline_context) { &context->condition_not_empty, &context->condition_not_full); } } else if (status == STATUS_CHUNK) { - receive_chunk_enqueue(file_descriptor, context); + if (!receive_chunk_enqueue(file_descriptor, context)) + return thrd_error; } else if (status == STATUS_CHECK_BATCH) { int count; if (!receive_int(file_descriptor, &count)) @@ -161,6 +163,7 @@ int receive_thread(void* pipeline_context) { &context->condition_not_empty, &context->condition_not_full); } else { log_message(LOG_LEVEL_ERROR, "Failed to receive file"); + return thrd_error; } } next: diff --git a/src/shared/protocol.c b/src/shared/protocol.c index 8bd7cfa..2a0535d 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -15,7 +15,7 @@ static __thread int io_read_fd = -1; static __thread int io_write_fd = -1; -static SSL* io_ssl; +static __thread SSL* io_ssl; static unsigned long long io_bwlimit = 0; static long long bw_tokens = 0; diff --git a/tests/runner.c b/tests/runner.c index 402af49..0bba0f1 100644 --- a/tests/runner.c +++ b/tests/runner.c @@ -1,11 +1,13 @@ #include "test_array_list.h" #include "test_chunk.h" +#include "test_client_cli.h" #include "test_compression.h" #include "test_config.h" #include "test_data.h" #include "test_delta.h" #include "test_file.h" #include "test_file_sendfile.h" +#include "test_fuzz_smoke.h" #include "test_glob.h" #include "test_log.h" #include "test_metadata.h" @@ -15,6 +17,7 @@ #include "test_queue.h" #include "test_robustness.h" #include "test_scanner.h" +#include "test_server.h" #include "test_shared_utils.h" #include "test_stress.h" #include "test_transport_tcp.h" @@ -53,6 +56,9 @@ int main() { RUN_TEST(test_transport_tcp); RUN_TEST(test_transport_ssh); RUN_TEST(test_transport_tls); + RUN_TEST(test_client_cli); + RUN_TEST(test_server); + RUN_TEST(test_fuzz_smoke); printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n"); printf("Total Tests Run: %d\n", tests_run); diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c new file mode 100644 index 0000000..ab44023 --- /dev/null +++ b/tests/test_client_cli.c @@ -0,0 +1,89 @@ +#include "test_client_cli.h" +#include "config.h" +#include "test_utils.h" +#include "utils.h" +#include +#include +#include + +/* Test main() with --help flag (early return path, no server connection needed) */ +static void test_cli_help() { + /* We can't easily call main() because it calls send_files which needs a server. + * Instead, test the argument parsing logic by testing that config_create works + * with the same parameters client_cli uses, and that config_delete cleans up + * properly when send_directory and receive_root_directory are NULL. */ + + /* This matches what client_cli does at startup */ + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + EXPECT_NULL(cfg->send_directory); + EXPECT_NULL(cfg->receive_root_directory); + EXPECT_FALSE(cfg->save_to_disk); + EXPECT_EQ_INT(cfg->compression_level, 5); + + config_delete(cfg); +} + +/* Test that --archive sets compression, multithreading, and metadata */ +static void test_cli_archive_flags() { + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + + /* Simulate --archive flag */ + cfg->use_compression = true; + cfg->use_multithreading = true; + cfg->use_metadata = true; + + EXPECT_TRUE(cfg->use_compression); + EXPECT_TRUE(cfg->use_multithreading); + EXPECT_TRUE(cfg->use_metadata); + + config_delete(cfg); +} + +/* Test that --dry-run sets dry_run flag */ +static void test_cli_dry_run() { + Config* cfg = config_create(); + + cfg->dry_run = true; + EXPECT_TRUE(cfg->dry_run); + + config_delete(cfg); +} + +/* Test that --delete sets use_delete */ +static void test_cli_delete_flag() { + Config* cfg = config_create(); + + cfg->use_delete = true; + EXPECT_TRUE(cfg->use_delete); + + config_delete(cfg); +} + +/* Test exclude pattern handling */ +static void test_cli_exclude_patterns() { + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + + /* Simulate --exclude "*.log" --exclude "tmp/" */ + cfg->exclude_patterns = malloc(2 * sizeof(char*)); + EXPECT_NOT_NULL(cfg->exclude_patterns); + cfg->exclude_patterns[0] = str_dup("*.log"); + cfg->exclude_patterns[1] = str_dup("tmp/"); + cfg->exclude_count = 2; + + EXPECT_EQ_STR(cfg->exclude_patterns[0], "*.log"); + EXPECT_EQ_STR(cfg->exclude_patterns[1], "tmp/"); + EXPECT_EQ_INT(cfg->exclude_count, 2); + + config_delete(cfg); +} + +void test_client_cli() { + test_cli_help(); + test_cli_archive_flags(); + test_cli_dry_run(); + test_cli_delete_flag(); + test_cli_exclude_patterns(); +} diff --git a/tests/test_client_cli.h b/tests/test_client_cli.h new file mode 100644 index 0000000..daacba5 --- /dev/null +++ b/tests/test_client_cli.h @@ -0,0 +1,6 @@ +#ifndef TEST_CLIENT_CLI_H +#define TEST_CLIENT_CLI_H + +void test_client_cli(); + +#endif diff --git a/tests/test_config.c b/tests/test_config.c index 2dad898..44b7332 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -1,14 +1,38 @@ #include "test_config.h" #include "config.h" #include "multiprocessing.h" +#include "protocol.h" #include "queue.h" #include "test_utils.h" #include "utils.h" #include +#include +#include +#include + +static Config* make_config(const char* version, const char* src, const char* dst, bool save, + bool mt, bool cs, bool comp, bool meta, int clevel, bool sf, + unsigned long long csize) { + Config* cfg = config_create(); + if (!cfg) + return NULL; + cfg->version = str_dup(version); + cfg->send_directory = str_dup(src); + cfg->receive_root_directory = str_dup(dst); + cfg->save_to_disk = save; + cfg->use_multithreading = mt; + cfg->use_chunk_serialization = cs; + cfg->use_compression = comp; + cfg->use_metadata = meta; + cfg->compression_level = clevel; + cfg->use_sendfile = sf; + if (csize > 0) + cfg->chunk_size = csize; + return cfg; +} static void test_config_lifecycle() { - Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/dst"), true, true, false, - false, false, 1, false, 0); + Config* cfg = make_config("1.0", "/src", "/dst", true, true, false, false, false, 1, false, 0); EXPECT_NOT_NULL(cfg); EXPECT_EQ_STR(cfg->version, "1.0"); EXPECT_EQ_STR(cfg->send_directory, "/src"); @@ -23,8 +47,8 @@ static void test_config_lifecycle() { } static void test_config_ssh_dest() { - Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("user@host:/dst"), true, - false, false, false, false, 1, false, 0); + Config* cfg = make_config("1.0", "/src", "user@host:/dst", true, false, false, false, false, 1, + false, 0); EXPECT_NOT_NULL(cfg); EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP); EXPECT_NULL(cfg->ssh_destination); @@ -38,8 +62,8 @@ static void test_config_ssh_dest() { } static void test_config_ssh_dest_local_path() { - Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/local/path"), true, false, - false, false, false, 1, false, 0); + Config* cfg = make_config("1.0", "/src", "/local/path", true, false, false, false, false, 1, + false, 0); config_parse_ssh_dest(cfg); EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP); EXPECT_NULL(cfg->ssh_destination); @@ -48,8 +72,8 @@ static void test_config_ssh_dest_local_path() { } static void test_config_ssh_dest_no_user() { - Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("host:/remote"), true, false, - false, false, false, 1, false, 0); + Config* cfg = make_config("1.0", "/src", "host:/remote", true, false, false, false, false, 1, + false, 0); config_parse_ssh_dest(cfg); EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH); EXPECT_EQ_STR(cfg->ssh_destination, "host:/remote"); @@ -58,8 +82,7 @@ static void test_config_ssh_dest_no_user() { } static void test_pipeline_sender_lifecycle() { - Config* cfg = config_create(str_dup("2.0"), str_dup("/src2"), str_dup("/dst2"), false, false, - true, true, false, 1, false, 0); + Config* cfg = make_config("2.0", "/src2", "/dst2", false, false, true, true, false, 1, false, 0); Queue* q1 = queue_create(5, NULL); Queue* q2 = queue_create(15, NULL); @@ -75,8 +98,7 @@ static void test_pipeline_sender_lifecycle() { } static void test_pipeline_receiver_lifecycle() { - Config* cfg = config_create(str_dup("3.0"), str_dup("/src3"), str_dup("/dst3"), true, true, true, - true, false, 1, false, 0); + Config* cfg = make_config("3.0", "/src3", "/dst3", true, true, true, true, false, 1, false, 0); Queue* q = queue_create(20, NULL); PipelineContextReceiver* pcr = pipeline_context_receiver_create(cfg, q, 42); @@ -89,6 +111,132 @@ static void test_pipeline_receiver_lifecycle() { pipeline_context_receiver_destroy(pcr); } +static void test_config_send_receive() { + /* Create a config to send */ + Config* send_cfg = config_create(); + EXPECT_NOT_NULL(send_cfg); + send_cfg->send_directory = str_dup("/send/src"); + send_cfg->receive_root_directory = str_dup("/send/dst"); + send_cfg->save_to_disk = true; + send_cfg->use_multithreading = true; + send_cfg->use_chunk_serialization = true; + send_cfg->use_compression = true; + send_cfg->use_metadata = true; + send_cfg->compression_level = 5; + send_cfg->chunk_size = 1024; + + /* Use pipe for communication */ + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: receive the config */ + close(p[1]); + Config* recv_cfg = config_receive(p[0]); + close(p[0]); + + bool ok = true; + if (!recv_cfg) + ok = false; + else { + if (strcmp(recv_cfg->version, PROTOCOL_VERSION) != 0) + ok = false; + if (strcmp(recv_cfg->send_directory, "/send/src") != 0) + ok = false; + if (strcmp(recv_cfg->receive_root_directory, "/send/dst") != 0) + ok = false; + if (!recv_cfg->save_to_disk) + ok = false; + if (!recv_cfg->use_multithreading) + ok = false; + if (!recv_cfg->use_chunk_serialization) + ok = false; + if (recv_cfg->compression_level != 5) + ok = false; + if (recv_cfg->chunk_size != 1024) + ok = false; + } + config_delete(recv_cfg); + _exit(ok ? 0 : 1); + } else { + /* Parent: send the config */ + close(p[0]); + bool sent = config_send(p[1], send_cfg); + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + config_delete(send_cfg); + + EXPECT_TRUE(sent); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +static void test_config_send_receive_version_mismatch() { + /* Create a config with a different protocol version */ + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + free(cfg->version); + cfg->version = str_dup("0.0"); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + close(p[1]); + /* Should fail because version "0.0" != PROTOCOL_VERSION */ + Config* recv = config_receive(p[0]); + close(p[0]); + /* recv should be NULL on version mismatch */ + _exit(recv == NULL ? 0 : 1); + } else { + close(p[0]); + bool sent = config_send(p[1], cfg); + close(p[1]); + + /* send should succeed (sends the config, receives ERROR on version mismatch) */ + int status; + waitpid(pid, &status, 0); + + config_delete(cfg); + + /* config_send returns false because it receives STATUS_ERROR back */ + EXPECT_FALSE(sent); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +static void test_is_remote_dest() { + /* Valid SSH-style destinations */ + EXPECT_TRUE(is_remote_dest("user@host:/path")); + EXPECT_TRUE(is_remote_dest("host:/path")); + EXPECT_TRUE(is_remote_dest("user@192.168.1.1:/remote/path")); + + /* Invalid destinations */ + EXPECT_FALSE(is_remote_dest(NULL)); + EXPECT_FALSE(is_remote_dest("")); + EXPECT_FALSE(is_remote_dest(":")); + EXPECT_FALSE(is_remote_dest("/local/path")); + EXPECT_FALSE(is_remote_dest("relative/path")); + EXPECT_FALSE(is_remote_dest("C:/windows/path")); + + /* Edge cases */ + EXPECT_FALSE(is_remote_dest("noslash")); + EXPECT_FALSE(is_remote_dest("/")); + EXPECT_TRUE(is_remote_dest("host:")); + EXPECT_TRUE(is_remote_dest("user@host:")); +} + void test_config() { test_config_lifecycle(); test_config_ssh_dest(); @@ -96,4 +244,9 @@ void test_config() { test_config_ssh_dest_no_user(); test_pipeline_sender_lifecycle(); test_pipeline_receiver_lifecycle(); + if (!is_running_under_valgrind()) { + test_config_send_receive(); + test_config_send_receive_version_mismatch(); + } + test_is_remote_dest(); } diff --git a/tests/test_file.c b/tests/test_file.c index 25862a5..68b2447 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -154,8 +154,11 @@ static void test_file_send_receive() { memcpy(file->data->data, content, len); file->data->size = len; - Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false, - false, false, false, false, 0, false, 0); + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/tmp"); + cfg->receive_root_directory = str_dup("/tmp"); int p[2]; EXPECT_EQ_INT(pipe(p), 0); @@ -261,6 +264,164 @@ static void test_file_metadata_create() { unlink("test_meta_file.txt"); } +static void test_file_save_to_disk_path_traversal() { + /* Test that path traversal is rejected */ + File* f = file_create("../etc/passwd"); + EXPECT_NOT_NULL(f); + const char* content = "should not save"; + f->data->data = malloc(strlen(content)); + EXPECT_NOT_NULL(f->data->data); + memcpy(f->data->data, content, strlen(content)); + f->data->size = strlen(content); + + /* file_save_to_disk should detect path traversal and return false */ + EXPECT_FALSE(file_save_to_disk("/tmp", f, NULL)); + + file_destroy(f); +} + +static void test_file_save_to_disk_deep_traversal() { + File* f = file_create("subdir/../../etc/passwd"); + EXPECT_NOT_NULL(f); + const char* content = "should not save"; + f->data->data = malloc(strlen(content)); + EXPECT_NOT_NULL(f->data->data); + memcpy(f->data->data, content, strlen(content)); + f->data->size = strlen(content); + + EXPECT_FALSE(file_save_to_disk("/tmp", f, NULL)); + + file_destroy(f); +} + +static void test_file_send_single_calls_compression() { + File* file = file_create("test_send_comp.txt"); + EXPECT_NOT_NULL(file); + const char* content = "Hello, Compressed File Transfer!"; + size_t len = strlen(content); + file->data->data = malloc(len); + EXPECT_NOT_NULL(file->data->data); + memcpy(file->data->data, content, len); + file->data->size = len; + + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/tmp"); + cfg->receive_root_directory = str_dup("/tmp"); + cfg->use_compression = true; + cfg->compression_level = 3; + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + close(p[1]); + File* received = file_receive(cfg, p[0]); + close(p[0]); + + bool ok = true; + if (!received) + ok = false; + else { + if (strcmp(received->path, "test_send_comp.txt") != 0) + ok = false; + if (!received->data || received->data->size != len) + ok = false; + else if (memcmp(received->data->data, content, len) != 0) + ok = false; + } + file_destroy(received); + config_delete(cfg); + _exit(ok ? 0 : 1); + } else { + close(p[0]); + bool sent = file_send_single_calls(file, p[1], false, 3, true); + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + file_destroy(file); + config_delete(cfg); + + EXPECT_TRUE(sent); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +static void test_file_send_single_calls_metadata_and_path() { + /* Create a real file on disk so we can have metadata */ + const char* content = "File with metadata"; + size_t len = strlen(content); + EXPECT_TRUE(to_disk("test_meta_send.txt", content, len)); + + struct stat st; + EXPECT_EQ_INT(stat("test_meta_send.txt", &st), 0); + + File* file = file_create("test_meta_send.txt"); + EXPECT_NOT_NULL(file); + file->data->size = len; + file->data->data = malloc(len); + EXPECT_NOT_NULL(file->data->data); + memcpy(file->data->data, content, len); + file->metadata = file_metadata_create(&st); + EXPECT_NOT_NULL(file->metadata); + + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/tmp"); + cfg->receive_root_directory = str_dup("/tmp"); + cfg->use_metadata = true; + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + close(p[1]); + File* received = file_receive(cfg, p[0]); + close(p[0]); + + bool ok = true; + if (!received) + ok = false; + else { + if (strcmp(received->path, "test_meta_send.txt") != 0) + ok = false; + if (!received->data || received->data->size != len) + ok = false; + else if (memcmp(received->data->data, content, len) != 0) + ok = false; + if (!received->metadata) + ok = false; + } + file_destroy(received); + config_delete(cfg); + _exit(ok ? 0 : 1); + } else { + close(p[0]); + bool sent = file_send_single_calls(file, p[1], true, 0, true); + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + file_destroy(file); + config_delete(cfg); + unlink("test_meta_send.txt"); + + EXPECT_TRUE(sent); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + void test_file() { test_file_create(); test_file_destroy_null(); @@ -271,6 +432,8 @@ void test_file() { test_to_disk_basic(); test_to_disk_creates_dirs(); test_file_content_to_buffer(); + test_file_save_to_disk_path_traversal(); + test_file_save_to_disk_deep_traversal(); if (!is_running_under_valgrind()) { // Fork tests are skipped under valgrind because the parent process runs // orders of magnitude slower than the child (parent is instrumented, child @@ -279,6 +442,8 @@ void test_file() { // forked children where inherited allocations are reported as leaks. test_file_send_receive(); test_file_send_no_path(); + test_file_send_single_calls_compression(); + test_file_send_single_calls_metadata_and_path(); } test_file_metadata_create(); } diff --git a/tests/test_file_sendfile.c b/tests/test_file_sendfile.c index 86119e4..0bec4c4 100644 --- a/tests/test_file_sendfile.c +++ b/tests/test_file_sendfile.c @@ -22,9 +22,11 @@ static void test_sendfile_basic() { /* Set the size so file_send_sendfile can report it */ file->data->size = len; - Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false, - false, false, false, false, 0, false, 0); + Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/tmp"); + cfg->receive_root_directory = str_dup("/tmp"); int p[2]; EXPECT_EQ_INT(pipe(p), 0); @@ -80,9 +82,11 @@ static void test_sendfile_empty_file() { EXPECT_NOT_NULL(file); file->data->size = 0; - Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false, - false, false, false, false, 0, false, 0); + Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/tmp"); + cfg->receive_root_directory = str_dup("/tmp"); int p[2]; EXPECT_EQ_INT(pipe(p), 0); @@ -161,8 +165,13 @@ static void test_sendfile_compression_fallback() { file->data->size = (size_t)st.st_size; EXPECT_TRUE(file_load_data(file)); - Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false, - false, false, true, false, 3, false, 0); + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/tmp"); + cfg->receive_root_directory = str_dup("/tmp"); + cfg->use_compression = true; + cfg->compression_level = 3; EXPECT_NOT_NULL(cfg); int p[2]; diff --git a/tests/test_fuzz_smoke.c b/tests/test_fuzz_smoke.c new file mode 100644 index 0000000..5f1b9ef --- /dev/null +++ b/tests/test_fuzz_smoke.c @@ -0,0 +1,176 @@ +#include "test_fuzz_smoke.h" +#include "chunk.h" +#include "compression.h" +#include "data.h" +#include "delta.h" +#include "metadata.h" +#include "test_utils.h" +#include "utils.h" +#include +#include +#include +#include + +/* Smoke test for chunk_deserialize fuzz target */ +static void test_fuzz_chunk_deserialize() { + /* Create a minimal valid chunk to serialize and deserialize */ + File* file = file_create("fuzz_test.txt"); + EXPECT_NOT_NULL(file); + const char* content = "fuzz data"; + file->data->data = malloc(strlen(content)); + EXPECT_NOT_NULL(file->data->data); + memcpy(file->data->data, content, strlen(content)); + file->data->size = strlen(content); + + File* chunk_files[] = {file}; + Chunk* chunk = chunk_create(chunk_files, 1); + EXPECT_NOT_NULL(chunk); + + Data* serialized = chunk_serialize(chunk, true); + EXPECT_NOT_NULL(serialized); + + /* Now deserialize (this is what the fuzzer does) */ + Chunk* deserialized = chunk_deserialize(serialized, true); + EXPECT_NOT_NULL(deserialized); + EXPECT_EQ_INT(deserialized->element_count, 1); + + chunk_destroy(deserialized); + data_destroy(serialized); + /* chunk_destroy will also destroy the file added to chunk */ + chunk_destroy(chunk); +} + +/* Smoke test for compress/decompress fuzz target */ +static void test_fuzz_compress_decompress() { + const char* test_data = "Hello, this is some test data for compression fuzzing!"; + size_t len = strlen(test_data); + + Data* original = data_create((void*)test_data, len); + EXPECT_NOT_NULL(original); + + /* Compress at level 3 */ + Data* compressed = data_compress(original, 3); + EXPECT_NOT_NULL(compressed); + EXPECT_TRUE(compressed->size < original->size || compressed->size == original->size + 64); + + /* Decompress */ + Data* decompressed = data_decompress(compressed); + EXPECT_NOT_NULL(decompressed); + EXPECT_EQ_INT((int)decompressed->size, (int)len); + EXPECT_EQ_INT(memcmp(decompressed->data, test_data, len), 0); + + data_destroy(decompressed); + data_destroy(compressed); + data_destroy(original); +} + +/* Smoke test for delta_deserialize fuzz target */ +static void test_fuzz_delta_deserialize() { + /* Create two buffers of data */ + const char* old_data_str = "Hello, World!"; + const char* new_data_str = "Hello, Delta!"; + size_t old_len = strlen(old_data_str); + size_t new_len = strlen(new_data_str); + + /* Create delta signature from old data */ + DeltaSignature* sig = delta_signature_create((void*)old_data_str, old_len, 64); + EXPECT_NOT_NULL(sig); + + /* Create delta from signature and new data */ + Delta* delta = delta_compute((void*)new_data_str, new_len, sig, 64); + EXPECT_NOT_NULL(delta); + EXPECT_EQ_INT((int)delta->new_file_size, (int)new_len); + + /* Serialize the delta */ + Data* serialized = delta_serialize(delta); + EXPECT_NOT_NULL(serialized); + + /* Deserialize (this is what the fuzzer does) */ + Delta* deserialized = delta_deserialize(serialized); + EXPECT_NOT_NULL(deserialized); + EXPECT_EQ_INT((int)deserialized->new_file_size, (int)new_len); + + delta_destroy(deserialized); + data_destroy(serialized); + delta_destroy(delta); + delta_signature_destroy(sig); +} + +/* Smoke test for metadata_from_buf fuzz target */ +static void test_fuzz_metadata_from_buf() { + /* Create a real file to get metadata from */ + EXPECT_TRUE(to_disk("fuzz_meta_test.txt", "metadata test", 13)); + + struct stat st; + EXPECT_EQ_INT(stat("fuzz_meta_test.txt", &st), 0); + + FileMetadata* meta = file_metadata_create(&st); + EXPECT_NOT_NULL(meta); + EXPECT_EQ_INT((int)meta->mode, (int)st.st_mode); + EXPECT_EQ_INT((int)meta->mtime_sec, (int)st.st_mtime); + + /* Serialize metadata to buffer using the same approach as chunk.c */ + size_t meta_buf_size = sizeof(int32_t) + FILE_METADATA_WIRE_SIZE; + char* meta_buf = malloc(meta_buf_size); + EXPECT_NOT_NULL(meta_buf); + char* meta_ptr = meta_buf; + metadata_to_buf(&meta_ptr, meta); + EXPECT_EQ_INT((int)(meta_ptr - meta_buf), (int)meta_buf_size); + + /* Deserialize from buffer (simulates fuzz_metadata_from_buf) */ + char* buf_copy = meta_buf; + FileMetadata* deserialized = metadata_from_buf(&buf_copy); + EXPECT_NOT_NULL(deserialized); + EXPECT_EQ_INT((int)deserialized->mode, (int)meta->mode); + EXPECT_EQ_INT((int)deserialized->mtime_sec, (int)meta->mtime_sec); + + file_metadata_destroy(deserialized); + free(meta_buf); + file_metadata_destroy(meta); + unlink("fuzz_meta_test.txt"); +} + +/* Smoke test for delta_signature_deserialize fuzz target */ +static void test_fuzz_delta_signature_deserialize() { + const char* data_str = "Test data for signature"; + size_t len = strlen(data_str); + + DeltaSignature* sig = delta_signature_create((void*)data_str, len, 64); + EXPECT_NOT_NULL(sig); + + /* Serialize */ + Data* serialized = delta_signature_serialize(sig); + EXPECT_NOT_NULL(serialized); + + /* Deserialize (simulates what the fuzzer tests) */ + DeltaSignature* deserialized = delta_signature_deserialize(serialized); + EXPECT_NOT_NULL(deserialized); + EXPECT_EQ_INT((int)deserialized->block_size, 64); + + delta_signature_destroy(deserialized); + data_destroy(serialized); + delta_signature_destroy(sig); +} + +/* Smoke test for glob_match fuzz target */ +static void test_fuzz_glob_match() { + /* Test various pattern matches */ + EXPECT_TRUE(glob_match("*.txt", "file.txt")); + EXPECT_TRUE(glob_match("*.txt", "file.TXT")); + EXPECT_FALSE(glob_match("*.txt", "file.c")); + EXPECT_TRUE(glob_match("data?", "data1")); + EXPECT_TRUE(glob_match("data?", "dataX")); + EXPECT_FALSE(glob_match("data?", "data12")); + EXPECT_TRUE(glob_match("src/**/*.c", "src/main.c")); + EXPECT_TRUE(glob_match("**/test*.py", "src/tests/test_foo.py")); + EXPECT_FALSE(glob_match("*.md", "readme.txt")); +} + +void test_fuzz_smoke() { + test_fuzz_chunk_deserialize(); + test_fuzz_compress_decompress(); + test_fuzz_delta_deserialize(); + test_fuzz_metadata_from_buf(); + test_fuzz_delta_signature_deserialize(); + test_fuzz_glob_match(); +} diff --git a/tests/test_fuzz_smoke.h b/tests/test_fuzz_smoke.h new file mode 100644 index 0000000..5699f45 --- /dev/null +++ b/tests/test_fuzz_smoke.h @@ -0,0 +1,6 @@ +#ifndef TEST_FUZZ_SMOKE_H +#define TEST_FUZZ_SMOKE_H + +void test_fuzz_smoke(); + +#endif diff --git a/tests/test_multiprocessing.c b/tests/test_multiprocessing.c index 741bcf0..f847846 100644 --- a/tests/test_multiprocessing.c +++ b/tests/test_multiprocessing.c @@ -1,16 +1,24 @@ #include "test_multiprocessing.h" #include "multiprocessing.h" #include "config.h" +#include "protocol.h" #include "queue.h" #include "utils.h" #include "test_utils.h" +#include #include +#include +#include +#include +#include /* Test pipeline_context_sender_create/destroy with valid arguments */ static void test_sender_create_destroy() { - Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/dst"), false, false, false, - false, false, 0, false, 0); + Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); + cfg->version = str_dup("1.0"); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); Queue* q_scanner = queue_create(5, NULL); EXPECT_NOT_NULL(q_scanner); @@ -32,9 +40,13 @@ static void test_sender_create_destroy() { /* Test pipeline_context_receiver_create/destroy with valid arguments */ static void test_receiver_create_destroy() { - Config* cfg = config_create(str_dup("2.0"), str_dup("/src"), str_dup("/dst"), true, true, false, - false, false, 0, false, 0); + Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); + cfg->version = str_dup("2.0"); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); + cfg->save_to_disk = true; + cfg->use_multithreading = true; Queue* q = queue_create(20, NULL); EXPECT_NOT_NULL(q); @@ -51,9 +63,11 @@ static void test_receiver_create_destroy() { /* Test that create handles various queue capacities */ static void test_sender_queue_capacities() { - Config* cfg = config_create(str_dup("3.0"), str_dup("/src"), str_dup("/dst"), false, false, false, - false, false, 0, false, 0); + Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); + cfg->version = str_dup("3.0"); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); /* Single-element queues */ Queue* q1 = queue_create(1, NULL); @@ -67,9 +81,11 @@ static void test_sender_queue_capacities() { /* Test that create handles zero-capacity queues */ static void test_sender_zero_capacity() { - Config* cfg = config_create(str_dup("4.0"), str_dup("/src"), str_dup("/dst"), false, false, false, - false, false, 0, false, 0); + Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); + cfg->version = str_dup("4.0"); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); Queue* q1 = queue_create(0, NULL); Queue* q2 = queue_create(0, NULL); @@ -82,8 +98,11 @@ static void test_sender_zero_capacity() { /* Test receiver with zero file_descriptor */ static void test_receiver_fd_zero() { - Config* cfg = config_create(str_dup("5.0"), str_dup("/src"), str_dup("/dst"), false, false, false, - false, false, 0, false, 0); + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup("5.0"); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); Queue* q = queue_create(5, NULL); PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, 0); EXPECT_NOT_NULL(ctx); @@ -92,10 +111,111 @@ static void test_receiver_fd_zero() { pipeline_context_receiver_destroy(ctx); } +/* Test that receive_thread completes cleanly when sent FINISHED immediately */ +static void test_receive_thread_finished() { + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/tmp/dst"); + cfg->save_to_disk = true; + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: run receive_thread */ + close(p[1]); + + Queue* q = queue_create(5, file_destroy); + EXPECT_NOT_NULL(q); + PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, p[0]); + EXPECT_NOT_NULL(ctx); + + int ret = receive_thread(ctx); + + pipeline_context_receiver_destroy(ctx); + close(p[0]); + _exit(ret == thrd_success ? 0 : 1); + } else { + /* Parent: send STATUS_FINISHED then STATUS_MANIFEST */ + close(p[0]); + + /* Send a STATUS_FINISHED to make receive_thread exit cleanly. + * receive_thread reads status, sees FINISHED, then exits loop. + * After the loop it expects STATUS_MANIFEST check, but we sent + * FINISHED so it will just return thrd_success. */ + send_status(p[1], STATUS_FINISHED); + + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + /* Parent must free its own copies of config (child has separate copies) */ + config_delete(cfg); + + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +/* Test that write_thread completes cleanly when queue signals done */ +static void test_write_thread_done() { + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/tmp/dst"); + cfg->save_to_disk = false; + + Queue* q = queue_create(5, file_destroy); + EXPECT_NOT_NULL(q); + + PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, 0); + EXPECT_NOT_NULL(ctx); + + /* Mark receiver as done BEFORE starting the thread so it exits immediately */ + ctx->receiver_done = true; + + thrd_t writer; + int ret = thrd_create(&writer, write_thread, ctx); + EXPECT_EQ_INT(ret, thrd_success); + + int result; + thrd_join(writer, &result); + EXPECT_EQ_INT(result, thrd_success); + + /* Don't call pipeline_context_receiver_destroy because it frees ctx + * and write_thread doesn't destroy ctx. Actually looking at the code: + * write_thread reads context fields but doesn't free anything. + * The caller is responsible for cleanup. So we need to clean up. + * But wait - write_thread takes ownership? Let me check... + * No, write_thread just processes and returns. The caller frees. + * + * However, pipeline_context_receiver_destroy will call config_delete + * and queue_destroy which would double-free since we created them + * in this test. Let me just free the context directly. */ + mtx_destroy(&ctx->mutex); + cnd_destroy(&ctx->condition_not_full); + cnd_destroy(&ctx->condition_not_empty); + free(ctx); + + /* q and cfg still need cleanup */ + queue_destroy(q); + config_delete(cfg); +} + void test_multiprocessing() { test_sender_create_destroy(); test_receiver_create_destroy(); test_sender_queue_capacities(); test_sender_zero_capacity(); test_receiver_fd_zero(); + if (!is_running_under_valgrind()) { + test_receive_thread_finished(); + } + test_write_thread_done(); } diff --git a/tests/test_server.c b/tests/test_server.c new file mode 100644 index 0000000..18c3d93 --- /dev/null +++ b/tests/test_server.c @@ -0,0 +1,174 @@ +#include "test_server.h" +#include "config.h" +#include "file.h" +#include "protocol.h" +#include "test_utils.h" +#include "utils.h" +#include +#include +#include +#include +#include + +/* Include server.c but rename main to avoid conflict with test runner's main */ +#define main server_main_ +#define FASTSYNC_SERVER_AS_LIB +#include "server.c" +#undef main + +/* Test receive_files with immediate FINISHED status */ +static void test_receive_files_finished() { + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/tmp/dst"); + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: run receive_files */ + close(p[1]); + + int ret = receive_files(cfg, p[0]); + close(p[0]); + config_delete(cfg); + _exit(ret == 0 ? 0 : 1); + } else { + /* Parent: send FINISHED then ok */ + close(p[0]); + + /* receive_files expects an initial status, then loops. + * If we send STATUS_FINISHED first, it won't enter the loop body + * (status == STATUS_FINISHED doesn't match any case). + * After the loop, it checks if status == STATUS_FINISHED -> yes. + * Then sends STATUS_OK and returns 0. */ + send_status(p[1], STATUS_FINISHED); + /* receive_files will send STATUS_OK back, read it */ + Status resp; + receive_status(p[1], &resp); + + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + config_delete(cfg); + + EXPECT_EQ_INT(resp, STATUS_OK); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +/* Test receive_files with STATUS_NEXT + file data */ +static void test_receive_files_single_file() { + const char* content = "Hello from server test!"; + size_t len = strlen(content); + + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/tmp/dst"); + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: receive_files will try to call file_receive */ + close(p[1]); + + int ret = receive_files(cfg, p[0]); + close(p[0]); + config_delete(cfg); + _exit(ret == 0 ? 0 : 1); + } else { + /* Parent: send a file */ + close(p[0]); + + /* Send initial status = STATUS_NEXT */ + send_status(p[1], STATUS_NEXT); + + /* Now send the file data */ + File* file = file_create("test_server_file.txt"); + EXPECT_NOT_NULL(file); + file->data->data = malloc(len); + EXPECT_NOT_NULL(file->data->data); + memcpy(file->data->data, content, len); + file->data->size = len; + + /* Send path, then data (no metadata since config has use_metadata=false) */ + send_str(p[1], file->path); + send_data(p[1], file->data); + + file_destroy(file); + + /* Now send FINISHED to complete */ + send_status(p[1], STATUS_FINISHED); + Status resp; + receive_status(p[1], &resp); + + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + config_delete(cfg); + + EXPECT_EQ_INT(resp, STATUS_OK); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +/* Test receive_files with STATUS_ABORT */ +static void test_receive_files_abort() { + Config* cfg = config_create(); + EXPECT_NOT_NULL(cfg); + cfg->version = str_dup(PROTOCOL_VERSION); + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/tmp/dst"); + + int p[2]; + EXPECT_EQ_INT(pipe(p), 0); + io_set_fds(p[0], p[1]); + io_set_bwlimit(0); + + pid_t pid = fork(); + if (pid == 0) { + close(p[1]); + int ret = receive_files(cfg, p[0]); + close(p[0]); + config_delete(cfg); + /* Should return -1 on abort */ + _exit(ret == -1 ? 0 : 1); + } else { + close(p[0]); + + /* Send STATUS_ABORT */ + send_status(p[1], STATUS_ABORT); + + close(p[1]); + + int status; + waitpid(pid, &status, 0); + + config_delete(cfg); + + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + } +} + +void test_server() { + if (!is_running_under_valgrind()) { + test_receive_files_finished(); + test_receive_files_single_file(); + test_receive_files_abort(); + } +} diff --git a/tests/test_server.h b/tests/test_server.h new file mode 100644 index 0000000..5ab72bd --- /dev/null +++ b/tests/test_server.h @@ -0,0 +1,6 @@ +#ifndef TEST_SERVER_H +#define TEST_SERVER_H + +void test_server(); + +#endif diff --git a/tests/test_transport_tcp.c b/tests/test_transport_tcp.c index 516879f..7ec97fb 100644 --- a/tests/test_transport_tcp.c +++ b/tests/test_transport_tcp.c @@ -1,6 +1,8 @@ #include "test_transport_tcp.h" +#include "protocol.h" #include "test_utils.h" #include "transport_tcp.h" +#include #include static void test_server_create_ephemeral() { @@ -35,9 +37,66 @@ static void test_client_delete_null() { client_delete(c); } +/* Test tcp_set_timeouts with valid values */ +static void test_tcp_set_timeouts() { + /* Just verify the function doesn't crash with edge cases */ + tcp_set_timeouts(0, 0); /* zero means "don't change" */ + tcp_set_timeouts(60, 20); /* normal values */ + tcp_set_timeouts(-1, -1); /* negative means "don't change" */ + /* If we got here without crashing, the test passes */ + EXPECT_TRUE(true); +} + +/* Test client_connect with an invalid host (should fail gracefully) */ +static void test_client_connect_invalid_host() { + Client* c = client_create(); + EXPECT_NOT_NULL(c); + + /* Use a non-routable IP that will fail connect quickly */ + bool ok = client_connect(c, "10.255.255.1", 9999); + EXPECT_FALSE(ok); + + client_disconnect(c); + client_delete(c); +} + +/* Test server_create with a specific port */ +static void test_server_create_specific_port() { + /* Port 0 = ephemeral, but try 0 and verify bind works */ + Server* s = server_create(0); + EXPECT_NOT_NULL(s); + EXPECT_TRUE(s->file_descriptor >= 0); + server_delete(&s); + EXPECT_NULL(s); +} + +/* Test server_create with invalid port (0 is valid for ephemeral) */ +static void test_server_delete_double() { + Server* s = server_create(0); + EXPECT_NOT_NULL(s); + server_delete(&s); + EXPECT_NULL(s); + /* Deleting again should be safe - pointer is already NULL */ + server_delete(&s); + EXPECT_NULL(s); +} + +/* Test client_disconnect followed by client_delete */ +static void test_client_disconnect_delete() { + Client* c = client_create(); + EXPECT_NOT_NULL(c); + client_disconnect(c); + client_delete(c); +} + void test_transport_tcp() { test_server_create_ephemeral(); test_server_delete_null(); test_client_create(); test_client_delete_null(); + test_tcp_set_timeouts(); + test_client_connect_invalid_host(); + test_server_create_specific_port(); + test_server_delete_double(); + test_client_disconnect_delete(); } diff --git a/tests/test_transport_tls.c b/tests/test_transport_tls.c index 657732d..5e21580 100644 --- a/tests/test_transport_tls.c +++ b/tests/test_transport_tls.c @@ -1,7 +1,10 @@ #include "test_transport_tls.h" +#include "protocol.h" #include "test_utils.h" #include "transport_tcp.h" #include "transport_tls.h" +#include +#include static void test_tls_global_init() { bool ok = tls_global_init(); @@ -18,7 +21,41 @@ static void test_server_create_tls_without_certs() { EXPECT_NULL(s); } +/* Test client_connect_tls with no server listening (should fail gracefully) */ +static void test_client_connect_tls_fail() { + /* Create a client to localhost on a high port with no server */ + Client* c = client_create(); + EXPECT_NOT_NULL(c); + + /* connect to localhost:1 (no server) - should fail as connect() fails first */ + bool ok = client_connect_tls(c, "127.0.0.1", 1, NULL, NULL, NULL); + EXPECT_FALSE(ok); + + /* Note: client_connect_tls internally calls connect() which sets up the socket. + * On failure it returns false but does NOT close the socket - we need to + * disconnect/delete the client. The socket fd may be in an undefined state + * after a failed connect, so we just call client_delete which closes it. */ + client_disconnect(c); + client_delete(c); +} + +/* Test server_create_tls with missing cert file paths (should still create ctx without certs) */ +static void test_server_create_tls_empty_certs() { + Server* s = server_create(0); + EXPECT_NOT_NULL(s); + + /* Empty string paths - SSL_CTX_use_certificate_file will fail, but function returns false */ + bool ok = server_create_tls(s, "", "", NULL); + EXPECT_FALSE(ok); + EXPECT_NULL(s->ssl_ctx); + + server_delete(&s); + EXPECT_NULL(s); +} + void test_transport_tls() { test_tls_global_init(); test_server_create_tls_without_certs(); + test_client_connect_tls_fail(); + test_server_create_tls_empty_certs(); }