diff --git a/CMakeLists.txt b/CMakeLists.txt index db6a15f..42c2353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,8 +79,9 @@ set(TEST_INCLUDES tests src/shared src/server src/client) # Monolithic test binary (backward compatible) file(GLOB TEST_SRCS "tests/test_*.c" "tests/runner.c") -add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c) +add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c src/client/client_cli.c) target_include_directories(tests PRIVATE ${TEST_INCLUDES}) +target_compile_definitions(tests PRIVATE FASTSYNC_TEST_BUILD) target_link_libraries(tests PRIVATE ${TEST_LIBS}) add_test(NAME unit_all COMMAND tests) diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 5ed58bb..711a449 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -12,6 +12,7 @@ #include #include +#ifndef FASTSYNC_TEST_BUILD /* 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) { @@ -22,6 +23,7 @@ static void parse_environment(const char** out_env_source, const char** out_env_ if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) *out_save_to_disk = true; } +#endif /* Parse a string as a positive integer, returning true on success. */ static bool parse_positive_int(const char* s, int* out_val) { @@ -84,12 +86,15 @@ 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) { +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], "-V") == 0 || strcmp(argv[i], "--version") == 0) { + printf("fastsync version %s\n", PROTOCOL_VERSION); + return 1; } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) { config->use_compression = true; config->use_multithreading = true; @@ -102,6 +107,10 @@ static int parse_args(Config* config, int argc, char* argv[], int* positional_ar fprintf(stderr, "Error: invalid --port/-p value: %s\n", argv[i]); return -1; } + if (config->ssh_port > 65535) { + fprintf(stderr, "Error: SSH port must be 1-65535\n"); + return -1; + } } else if (strcmp(argv[i], "--delete") == 0) { config->use_delete = true; } else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) { @@ -220,6 +229,10 @@ static int parse_args(Config* config, int argc, char* argv[], int* positional_ar fprintf(stderr, "Error: invalid --server-port value: %s\n", argv[i]); return -1; } + if (config->server_port > 65535) { + fprintf(stderr, "Error: server port must be 1-65535\n"); + return -1; + } } else if (strcmp(argv[i], "--bwlimit") == 0 && i + 1 < argc) { char* end; errno = 0; @@ -451,6 +464,7 @@ static int parse_args(Config* config, int argc, char* argv[], int* positional_ar return 0; } +#ifndef FASTSYNC_TEST_BUILD /* Validate config after parsing. Returns true if valid. */ static bool validate_config(const Config* config) { if (!config->send_directory || !config->receive_root_directory) { @@ -491,6 +505,7 @@ static bool validate_config(const Config* config) { } return true; } +#endif /* FASTSYNC_TEST_BUILD */ static void print_usage(void) { printf("Usage:\n"); @@ -599,6 +614,7 @@ static void print_usage(void) { printf(" --copy-dest Copy destination\n"); printf(" --link-dest Link destination\n"); printf(" --help Show this help\n"); + printf(" -V, --version Show version\n"); } static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) { @@ -643,6 +659,7 @@ static int read_patterns_from_file(const char* filepath, char*** patterns, int* return 0; } +#ifndef FASTSYNC_TEST_BUILD int main(int argc, char* argv[]) { const char* env_source = NULL; const char* env_dest = NULL; @@ -748,3 +765,4 @@ cleanup: } return exit_code; } +#endif /* FASTSYNC_TEST_BUILD */ diff --git a/src/shared/config.c b/src/shared/config.c index c7900cc..cc8b8e6 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -8,10 +8,7 @@ #include #include -Config* config_create(void) { - Config* config = malloc(sizeof(Config)); - if (!config) - return NULL; +static void config_set_defaults(Config* config) { config->version = str_dup(PROTOCOL_VERSION); config->send_directory = NULL; config->receive_root_directory = NULL; @@ -101,6 +98,13 @@ Config* config_create(void) { config->server_mode = false; config->checksum = false; config->compress_choice = NULL; +} + +Config* config_create(void) { + Config* config = malloc(sizeof(Config)); + if (!config) + return NULL; + config_set_defaults(config); return config; } @@ -208,7 +212,7 @@ bool config_send(int file_descriptor, const Config* config) { return false; if (!send_int(file_descriptor, config->use_delta)) return false; - if (!send_int(file_descriptor, (int)config->delta_block_size)) + if (!send_n_data(file_descriptor, &config->delta_block_size, sizeof(config->delta_block_size))) return false; if (!send_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long))) return false; @@ -281,9 +285,11 @@ Config* config_receive(int file_descriptor) { Config* config = (Config*)malloc(sizeof(Config)); if (config == NULL) return NULL; - memset(config, 0, sizeof(*config)); + config_set_defaults(config); + free(config->version); config->version = receive_str(file_descriptor); if (!config->version) { + free(config->server_host); free(config); return NULL; } @@ -291,6 +297,7 @@ Config* config_receive(int file_descriptor) { fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", config->version, PROTOCOL_VERSION); free(config->version); + free(config->server_host); free(config); send_status(file_descriptor, STATUS_ERROR); return NULL; @@ -298,6 +305,7 @@ Config* config_receive(int file_descriptor) { config->send_directory = receive_str(file_descriptor); if (!config->send_directory) { free(config->version); + free(config->server_host); free(config); return NULL; } @@ -305,6 +313,7 @@ Config* config_receive(int file_descriptor) { if (!config->receive_root_directory) { free(config->version); free(config->send_directory); + free(config->server_host); free(config); return NULL; } @@ -341,67 +350,10 @@ Config* config_receive(int file_descriptor) { if (!receive_int(file_descriptor, &tmp)) goto error; config->use_delta = tmp; - if (!receive_int(file_descriptor, &tmp)) + if (!receive_n_data(file_descriptor, &config->delta_block_size, sizeof(config->delta_block_size))) goto error; - config->delta_block_size = (uint32_t)tmp; if (!receive_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long))) goto error; - config->show_progress = false; - config->dry_run = false; - config->ssh_port = 22; - config->transport = TRANSPORT_TCP; - config->ssh_destination = NULL; - config->fastsync_server_path = NULL; - config->exclude_patterns = NULL; - config->exclude_count = 0; - config->include_patterns = NULL; - config->include_count = 0; - config->max_size = 0; - config->min_size = 0; - config->use_tls = false; - config->tls_cert = NULL; - config->tls_key = NULL; - config->tls_ca = NULL; - config->timeout = 30; - config->contimeout = 10; - config->quiet = false; - config->stats = false; - config->max_depth = 0; - config->log_file = NULL; - config->queue_size = 100; - config->follow_symlinks = false; - config->copy_links = false; - config->safe_links = false; - config->copy_unsafe_links = false; - config->preserve_hard_links = false; - config->preserve_acls = false; - config->preserve_xattrs = false; - config->preserve_devices = false; - config->preserve_sparse = false; - config->itemize_changes = false; - config->out_format = NULL; - config->info_level = 0; - config->debug_level = 0; - config->list_only = false; - config->human_readable = false; - config->update = false; - config->inplace = false; - config->append = false; - config->append_verify = false; - config->delete_excluded = false; - config->delete_after = false; - config->max_delete = 0; - config->filters = NULL; - config->files_from = NULL; - config->cvs_exclude = false; - config->prune_empty_dirs = false; - config->relative = false; - config->rsh_command = NULL; - config->rsync_path = NULL; - config->temp_dir = NULL; - config->compare_dest = NULL; - config->copy_dest = NULL; - config->link_dest = NULL; if (!receive_int(file_descriptor, &tmp)) goto error; config->backup = tmp; @@ -482,15 +434,6 @@ Config* config_receive(int file_descriptor) { config->compress_choice = receive_str(file_descriptor); if (config->compress_choice == NULL) goto error; - config->address = NULL; - config->bind_address = NULL; - config->ipv6 = false; - config->ipv4 = false; - config->daemon = false; - config->daemon_config = NULL; - config->server_mode = false; - config->server_host = str_dup("127.0.0.1"); - config->server_port = 8080; if (!send_status(file_descriptor, STATUS_OK)) goto error; return config; diff --git a/src/shared/delta.c b/src/shared/delta.c index 0305fdc..f9f7c84 100644 --- a/src/shared/delta.c +++ b/src/shared/delta.c @@ -388,6 +388,10 @@ Delta* delta_deserialize(const Data* data) { if (type == DELTA_OP_BLOCK_MATCH) { if (pos + sizeof(uint32_t) * 3 > data->size) { + for (uint32_t k = 0; k < i; k++) { + if (delta->instructions[k].type == DELTA_INSTR_LITERAL) + free(delta->instructions[k].literal.data); + } free(delta->instructions); free(delta); return NULL; @@ -426,6 +430,11 @@ Delta* delta_deserialize(const Data* data) { } delta->instructions[i].literal.data = malloc(lit_len); if (!delta->instructions[i].literal.data) { + log_message(LOG_LEVEL_ERROR, "Failed to allocate %u bytes for literal data", lit_len); + for (uint32_t k = 0; k < i; k++) { + if (delta->instructions[k].type == DELTA_INSTR_LITERAL) + free(delta->instructions[k].literal.data); + } free(delta->instructions); free(delta); return NULL; diff --git a/src/shared/protocol.c b/src/shared/protocol.c index 0d41808..bce9507 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -10,7 +10,6 @@ #include #include -#define MAX_DATA_SIZE (100ULL * 1024 * 1024) /* 100 MB max per data message */ #define RECEIVE_TIMEOUT_SEC 60 /* 60 second per-message timeout */ #define MAX_CONNECTION_MEMORY (1024ULL * 1024 * 1024) /* 1 GB total per connection */ @@ -236,9 +235,9 @@ Data* receive_data(int file_descriptor) { unsigned long long size = 0; if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long))) return NULL; - if (size > MAX_DATA_SIZE) { + if (size > MAX_DATA_PAYLOAD_SIZE) { log_message(LOG_LEVEL_ERROR, "Data size %llu exceeds maximum %llu", size, - (unsigned long long)MAX_DATA_SIZE); + (unsigned long long)MAX_DATA_PAYLOAD_SIZE); return NULL; } if (total_allocated_bytes + size > MAX_CONNECTION_MEMORY) { diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index ab44023..cf13dfe 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -6,6 +6,9 @@ #include #include +/* Declaration of parse_args from client_cli.c */ +int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count); + /* 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. @@ -80,10 +83,160 @@ static void test_cli_exclude_patterns() { config_delete(cfg); } +/* Test parse_args with --help returns 1 (clean exit) */ +static void test_parse_args_help() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--help"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 2, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 1); + + config_delete(cfg); +} + +/* Test parse_args with -V/--version returns 1 */ +static void test_parse_args_version() { + Config* cfg = config_create(); + char* argv_short[] = {"fastsync", "-V"}; + char* argv_long[] = {"fastsync", "--version"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 2, argv_short, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 1); + + ret = parse_args(cfg, 2, argv_long, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 1); + + config_delete(cfg); +} + +/* Test parse_args with valid port */ +static void test_parse_args_valid_port() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-p", "2222", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 0); + EXPECT_EQ_INT(cfg->ssh_port, 2222); + EXPECT_EQ_INT(positional_count, 2); + + config_delete(cfg); +} + +/* Test parse_args rejects port > 65535 */ +static void test_parse_args_invalid_port() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-p", "99999", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, -1); + + config_delete(cfg); +} + +/* Test parse_args rejects non-numeric port */ +static void test_parse_args_non_numeric_port() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-p", "abc", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, -1); + + config_delete(cfg); +} + +/* Test parse_args rejects server port > 65535 */ +static void test_parse_args_invalid_server_port() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--server-port", "70000", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, -1); + + config_delete(cfg); +} + +/* Test parse_args rejects invalid compression level */ +static void test_parse_args_invalid_compression_level() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-c", "25", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, -1); + + config_delete(cfg); +} + +/* Test parse_args accepts valid compression level */ +static void test_parse_args_valid_compression_level() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-c", "10", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 0); + EXPECT_EQ_INT(cfg->compression_level, 10); + + config_delete(cfg); +} + +/* Test parse_args unknown option returns error */ +static void test_parse_args_unknown_option() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--nonexistent", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 4, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, -1); + + config_delete(cfg); +} + +/* Test parse_args with --archive flag */ +static void test_parse_args_archive() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--archive", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + int ret = parse_args(cfg, 4, argv, positional_args, &positional_count); + EXPECT_EQ_INT(ret, 0); + EXPECT_TRUE(cfg->use_compression); + EXPECT_TRUE(cfg->use_multithreading); + EXPECT_TRUE(cfg->use_metadata); + + 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(); + test_parse_args_help(); + test_parse_args_version(); + test_parse_args_valid_port(); + test_parse_args_invalid_port(); + test_parse_args_non_numeric_port(); + test_parse_args_invalid_server_port(); + test_parse_args_invalid_compression_level(); + test_parse_args_valid_compression_level(); + test_parse_args_unknown_option(); + test_parse_args_archive(); }