From beb681c2dc2a8dc88fc0c09bde770939d6c81304 Mon Sep 17 00:00:00 2001 From: TapTap Date: Sat, 5 Sep 2026 12:58:25 +0200 Subject: [PATCH 1/2] fix: #260 remove dead receive_files() and mkdir_r() receive_files() in src/server/server.c was a non-static, unprototyped, zero-caller duplicate of receiver_process()/receiver_receive_files() in src/server/receiver.c. Delete it along with the includes it uniquely pulled (chunk.h, metadata.h, sys/stat.h, duplicate quoted unistd.h); remaining code still uses file.h/config.h/protocol.h (via multiprocessing.h) directly. mkdir_r() in src/shared/utils.c had zero callers in src/ and tests/; remove the function and its declaration in utils.h, plus the unused libgen.h include. --- src/server/server.c | 138 -------------------------------------------- src/shared/utils.c | 40 ------------- src/shared/utils.h | 1 - 3 files changed, 179 deletions(-) diff --git a/src/server/server.c b/src/server/server.c index c968115..1ec365b 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -1,14 +1,11 @@ #include "config.h" -#include "chunk.h" #include "file.h" #include "log.h" -#include "metadata.h" #include "multiprocessing.h" #include "queue.h" #include "receiver.h" #include "transport_tcp.h" #include "transport_tls.h" -#include "unistd.h" #include "utils.h" #include #include @@ -16,7 +13,6 @@ #include #include #include -#include #include #include @@ -101,140 +97,6 @@ static bool __attribute__((unused)) configure_authorization(const char* root) { return true; } -int receive_files(Config* config, int fd) { - Status status; - if (!receive_status(fd, &status)) - return -1; - - while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || - status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) { - if (status == STATUS_KEEPALIVE) { - send_status(fd, STATUS_KEEPALIVE); - goto next; - } - if (status == STATUS_ABORT) { - log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up"); - return -1; - } - if (status == STATUS_CHECK) { - bool skipped; - File* file = receive_incremental_check(fd, config, &skipped); - if (skipped) - goto next; - if (file == NULL && !skipped) - return -1; - if (config->save_to_disk && - !file_save_to_disk(config->receive_root_directory, file, config)) { - file_destroy(file); - send_status(fd, STATUS_ERROR); - return -1; - } - file_destroy(file); - } else if (status == STATUS_CHUNK) { - Chunk* chunk = receive_chunk_data(fd, config); - if (chunk == NULL) { - send_status(fd, STATUS_ERROR); - return -1; - } - for (int i = 0; i < chunk->element_count; i++) { - if (config->save_to_disk && - !file_save_to_disk(config->receive_root_directory, chunk->items[i], config)) { - chunk_destroy(chunk); - send_status(fd, STATUS_ERROR); - return -1; - } - } - chunk_destroy(chunk); - } else if (status == STATUS_CHECK_BATCH) { - int count; - /* Batch framing has no checksum field yet; never silently downgrade a - checksum-enabled transfer into mtime-only matching. */ - if (config->checksum || !receive_int(fd, &count) || count < 0 || count > MAX_MANIFEST_ENTRIES) - return -1; - for (int i = 0; i < count; i++) { - char* check_path = receive_str(fd); - if (!check_path) - return -1; - unsigned long long check_size; - long long check_mtime; - if (!receive_n_data(fd, &check_size, sizeof(check_size)) || - !receive_n_data(fd, &check_mtime, sizeof(check_mtime))) { - free(check_path); - return -1; - } - long long check_mtime_nsec; - if (!receive_n_data(fd, &check_mtime_nsec, sizeof(check_mtime_nsec)) || - check_mtime_nsec < 0 || check_mtime_nsec >= 1000000000LL) { - free(check_path); - send_status(fd, STATUS_ERROR); - return -1; - } - if (!utils_valid_batch_path(check_path)) { - free(check_path); - send_status(fd, STATUS_ERROR); - return -1; - } - struct stat st; - char* full_path = path_cat(config->receive_root_directory, check_path); - if (!full_path) { - free(check_path); - send_status(fd, STATUS_ERROR); - return -1; - } - bool has_old = full_path && file_stat_secure(full_path, &st); - long long old_mtime_nsec = 0; - if (has_old) { -#ifdef __linux__ - old_mtime_nsec = st.st_mtim.tv_nsec; -#endif - } - bool match = !config->ignore_times && has_old && - (unsigned long long)st.st_size == check_size && - metadata_mtime_matches(st.st_mtime, old_mtime_nsec, (time_t)check_mtime, - (long)check_mtime_nsec, config->modify_window); - bool sent = send_status(fd, match ? STATUS_OK : STATUS_NEXT); - free(full_path); - free(check_path); - if (!sent) - return -1; - } - goto next; - } else { - File* file = file_receive(config, fd); - if (file == NULL) { - log_message(LOG_LEVEL_ERROR, "Failed to receive file"); - send_status(fd, STATUS_ERROR); - return -1; - } - if (config->save_to_disk && - !file_save_to_disk(config->receive_root_directory, file, config)) { - file_destroy(file); - send_status(fd, STATUS_ERROR); - return -1; - } - file_destroy(file); - } - next: - if (!receive_status(fd, &status)) { - send_status(fd, STATUS_ERROR); - return -1; - } - } - - if (status == STATUS_MANIFEST) { - if (receive_manifest(fd, config, &status) != 0) { - return -1; - } - } - if (status != STATUS_FINISHED) { - log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status"); - send_status(fd, STATUS_ERROR); - return -1; - } - send_status(fd, STATUS_OK); - return 0; -} - void handler(int file_descriptor) { SSL* ssl = io_get_ssl(); ProtocolSession session; diff --git a/src/shared/utils.c b/src/shared/utils.c index 6250a7a..9d37bb8 100644 --- a/src/shared/utils.c +++ b/src/shared/utils.c @@ -1,6 +1,5 @@ #include "utils.h" #include "array_list.h" -#include "libgen.h" #include "log.h" #include #include @@ -83,45 +82,6 @@ static int open_authorized_destination(const char* dest_root) { return dirfd; } -bool mkdir_r(const char* path) { - if (!path || *path == '\0') - return false; - char* duplicate = str_dup(path); - if (!duplicate) - return false; - int dirfd = open(path[0] == '/' ? "/" : ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); - if (dirfd < 0) { - free(duplicate); - return false; - } - bool ok = true; - char* saveptr = NULL; - char* component = strtok_r(duplicate, "/", &saveptr); - while (component) { - if (strcmp(component, "..") == 0) { - ok = false; - break; - } - if (strcmp(component, ".") != 0) { - int next = openat(dirfd, component, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); - if (next < 0 && errno == ENOENT) { - if (mkdirat(dirfd, component, 0755) == 0 || errno == EEXIST) - next = openat(dirfd, component, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); - } - if (next < 0) { - ok = false; - break; - } - close(dirfd); - dirfd = next; - } - component = strtok_r(NULL, "/", &saveptr); - } - close(dirfd); - free(duplicate); - return ok; -} - char* str_dup(const char* string) { if (string == NULL) return NULL; diff --git a/src/shared/utils.h b/src/shared/utils.h index 8202324..f9d76b4 100644 --- a/src/shared/utils.h +++ b/src/shared/utils.h @@ -5,7 +5,6 @@ #include #include -bool mkdir_r(const char* path); char* str_dup(const char* string); char* output_escape(const char* string, bool eight_bit_output); char* path_cat(const char* path1, const char* path2); From 1d61a1426ed1bb2153dd1054a28fc55ebddcb6cb Mon Sep 17 00:00:00 2001 From: TapTap Date: Sat, 5 Sep 2026 12:58:30 +0200 Subject: [PATCH 2/2] fix: #260 accept --opt=value uniformly and report missing arguments - Correct misleading doc comments on set_string_option / set_positive_int_option / set_nonneg_int_option (they return 0/-1, not true/false). - find_table_option_with_equals() now matches every OPTION_TABLE value option (OPT_STRING/OPT_POS_INT/OPT_NONNEG_INT/OPT_ULL), so forms such as --max-size=2G, --min-size=1K, --suffix=.bak, --timeout=30, --max-depth=5, --backup-dir=X parse instead of dying as 'Unknown option'. --max-size/--min-size now accept rsync-style binary suffixes (0 remains a valid 'no limit' byte count). Existing special handling for --compress-choice, --compress-level, --modify-window=, --chmod=, --skip-compress=, --compress-threads= is preserved. - Options that require a separate value (-p, --exclude, --include, --delta-block, --delta-max, --server-port, --bwlimit, --chunk-size, --log-file, --exclude-from, --include-from, -T, --skip-compress, --compress-threads) now emit an explicit 'missing argument' diagnostic instead of falling through to the generic 'Unknown option' branch when given as the final argv entry. - Add unit tests covering the = forms (--max-size=2G, --min-size=1K, --suffix=.bak, --timeout=30, --max-depth=5, --backup-dir=X) and a clean 'missing argument' (not 'Unknown option') diagnostic for trailing --exclude/--server-port/--skip-compress/-T. --- src/client/client_cli.c | 114 ++++++++++++++++++++++++++++++++-------- tests/test_client_cli.c | 67 +++++++++++++++++++++++ 2 files changed, 159 insertions(+), 22 deletions(-) diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 4caf6fb..fea3203 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -57,7 +57,7 @@ static bool parse_positive_int(const char* s, int* out_val) { return true; } -/* Duplicate a string argument into *dest, freeing the old value. Returns true on success, false on +/* Duplicate a string argument into *dest, freeing the old value. Returns 0 on success, -1 on * failure. */ static int set_string_option(char** dest, const char* value, const char* option_name) { char* dup = str_dup(value); @@ -70,7 +70,7 @@ static int set_string_option(char** dest, const char* value, const char* option_ return 0; } -/* Parse a string as a positive integer into *dest. Returns true on success, false on error. */ +/* Parse a string as a positive integer into *dest. Returns 0 on success, -1 on error. */ static int set_positive_int_option(int* dest, const char* value, const char* option_name) { if (!parse_positive_int(value, dest)) { log_message(LOG_LEVEL_ERROR, "%s must be a positive integer", option_name); @@ -102,7 +102,7 @@ static int set_compression_threads_option(int* dest, const char* value) { return 0; } -/* Parse a string as a non-negative integer into *dest. Returns true on success, false on error. */ +/* Parse a string as a non-negative integer into *dest. Returns 0 on success, -1 on error. */ static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) { if (!parse_nonneg_int(value, dest)) { log_message(LOG_LEVEL_ERROR, "%s must be a non-negative integer", option_name); @@ -237,7 +237,10 @@ static int parse_ull_arg(const char* val, unsigned long long* out, const char* o return 0; } -static int parse_size_arg(const char* value, unsigned long long* out) { +/* Parse a byte count with an optional single-letter binary suffix (K/M/G/T/P/E). + * When allow_zero is false, a bare 0 is rejected (size limits use true, since 0 + * means "no limit"). Returns 0 on success, -1 on error. */ +static int parse_size_arg_allow_zero(const char* value, unsigned long long* out, bool allow_zero) { if (!value || *value < '0' || *value > '9') return -1; char* end; @@ -281,12 +284,16 @@ static int parse_size_arg(const char* value, unsigned long long* out) { return -1; } } - if (number == 0 || number > ULLONG_MAX / multiplier) + if ((!allow_zero && number == 0) || number > ULLONG_MAX / multiplier) return -1; *out = number * multiplier; return 0; } +static int parse_size_arg(const char* value, unsigned long long* out) { + return parse_size_arg_allow_zero(value, out, false); +} + /* Append a duplicated pattern to a growable pattern array. Returns 0 on success, -1 on error. */ static int config_add_pattern(char*** patterns, int* count, const char* value, const char* optname) { @@ -450,6 +457,8 @@ static const OptionEntry* find_table_option(const char* arg) { return NULL; } +/* Match a "--opt=value" argument against table options that take a value. Flags, + * no-ops, and unsupported options do not accept an inline "=" value. */ static const OptionEntry* find_table_option_with_equals(const char* arg, const char** value) { const char* equals = strchr(arg, '='); if (!equals || equals == arg) @@ -460,8 +469,8 @@ static const OptionEntry* find_table_option_with_equals(const char* arg, const c if ((strlen(entry->name) == name_len && strncmp(arg, entry->name, name_len) == 0) || (entry->alias && strlen(entry->alias) == name_len && strncmp(arg, entry->alias, name_len) == 0)) { - if (strcmp(entry->name, "--compress-choice") == 0 || - strcmp(entry->name, "--compress-level") == 0) { + if (entry->kind == OPT_STRING || entry->kind == OPT_POS_INT || + entry->kind == OPT_NONNEG_INT || entry->kind == OPT_ULL) { *value = equals + 1; return entry; } @@ -516,8 +525,13 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch return set_nonneg_int_option((int*)field, value, entry->name); case OPT_ULL: { unsigned long long v; - if (parse_ull_arg(value, &v, entry->name) != 0) + /* Size-limit options accept rsync-style suffixes (e.g. --max-size=2G); a + * plain byte count, including 0 ("no limit"), stays valid. */ + if (parse_size_arg_allow_zero(value, &v, true) != 0) { + log_message(LOG_LEVEL_ERROR, "%s must be a non-negative size (B, K, M, G, T, P, or E)", + entry->name); return -1; + } *(unsigned long long*)field = v; return 0; } @@ -667,22 +681,38 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, config->use_multithreading = true; config->use_metadata = true; log_info_message(LOG_INFO_MISC, "Enabled archive mode (-c -m -M)"); - } else if (opt_is(argv[i], "-p", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "-p", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (set_positive_int_option(&config->ssh_port, argv[++i], "-p") != 0) return -1; if (config->ssh_port > 65535) { log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535"); return -1; } - } else if (opt_is(argv[i], "--exclude", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--exclude", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (config_add_pattern(&config->exclude_patterns, &config->exclude_count, argv[++i], "--exclude") != 0) return -1; - } else if (opt_is(argv[i], "--include", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--include", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (config_add_pattern(&config->include_patterns, &config->include_count, argv[++i], "--include") != 0) return -1; - } else if (opt_is(argv[i], "--delta-block", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--delta-block", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } unsigned long long val; if (parse_ull_arg(argv[++i], &val, "--delta-block") != 0) return -1; @@ -690,7 +720,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, config->delta_block_size = (uint32_t)val; else log_message(LOG_LEVEL_WARNING, "--delta-block value %llu out of range, using default", val); - } else if (opt_is(argv[i], "--delta-max", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--delta-max", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } unsigned long long val; if (parse_ull_arg(argv[++i], &val, "--delta-max") != 0) return -1; @@ -731,7 +765,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (opt_is(argv[i], "-s", NULL)) { config->use_chunk_serialization = true; log_info_message(LOG_INFO_MISC, "Enabled Chunk Serialization"); - } else if (opt_is(argv[i], "--server-port", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--server-port", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (!parse_positive_int(argv[++i], &config->server_port)) { char* escaped = output_escape(argv[i], false); log_message(LOG_LEVEL_ERROR, "invalid --server-port value: %s", @@ -743,7 +781,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, log_message(LOG_LEVEL_ERROR, "server port must be 1-65535"); return -1; } - } else if (opt_is(argv[i], "--bwlimit", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--bwlimit", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } unsigned long long kbps; if (parse_ull_arg(argv[++i], &kbps, "--bwlimit") != 0) return -1; @@ -757,7 +799,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } io_set_bwlimit(kbps * 1024); log_info_message(LOG_INFO_MISC, "Set bandwidth limit to %llu KB/s", kbps); - } else if (opt_is(argv[i], "--chunk-size", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--chunk-size", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } unsigned long long val; if (parse_ull_arg(argv[++i], &val, "--chunk-size") != 0) return -1; @@ -766,7 +812,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, return -1; } config->chunk_size = val; - } else if (opt_is(argv[i], "--log-file", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--log-file", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (config->log_file) { fclose(config->log_file); config->log_file = NULL; @@ -788,11 +838,19 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (opt_is(argv[i], "--stderr", NULL)) { if (i + 1 >= argc || set_stderr_mode(argv[++i]) != 0) return -1; - } else if (opt_is(argv[i], "--exclude-from", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--exclude-from", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != 0) return -1; - } else if (opt_is(argv[i], "--include-from", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--include-from", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (read_patterns_from_file(argv[++i], &config->include_patterns, &config->include_count) != 0) return -1; @@ -817,16 +875,28 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (opt_is(argv[i], "--info", NULL)) { if (i + 1 >= argc || parse_info_flags(argv[++i], config) != 0) return -1; - } else if (opt_is(argv[i], "-T", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "-T", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; } else if (strncmp(argv[i], "--skip-compress=", 16) == 0) { if (parse_skip_compress(config, argv[i] + 16) != 0) return -1; - } else if (opt_is(argv[i], "--skip-compress", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--skip-compress", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (parse_skip_compress(config, argv[++i]) != 0) return -1; - } else if (opt_is(argv[i], "--compress-threads", NULL) && i + 1 < argc) { + } else if (opt_is(argv[i], "--compress-threads", NULL)) { + if (i + 1 >= argc) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]); + return -1; + } if (set_compression_threads_option(&config->compression_threads, argv[++i]) != 0) return -1; } else if (opt_is(argv[i], "--checksum-choice", "--cc")) { diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 16e65c8..b53a161 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -1088,6 +1088,70 @@ static void test_parse_args_rejects_invalid_compression_choice() { config_delete(cfg); } +/* Every value-taking table option accepts an inline "--opt=value" form. */ +static void test_parse_args_table_equals_size_options() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--max-size=2G", "--min-size=1K", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); + EXPECT_TRUE(cfg->max_size == 2ULL * 1024 * 1024 * 1024); + EXPECT_TRUE(cfg->min_size == 1024ULL); + EXPECT_EQ_INT(positional_count, 2); + config_delete(cfg); +} + +static void test_parse_args_table_equals_string_and_int_options() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--suffix=.bak", "--timeout=30", "--max-depth=5", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 6, argv, positional_args, &positional_count), 0); + EXPECT_EQ_STR(cfg->suffix, ".bak"); + EXPECT_EQ_INT(cfg->timeout, 30); + EXPECT_EQ_INT(cfg->max_depth, 5); + EXPECT_EQ_INT(positional_count, 2); + config_delete(cfg); + + cfg = config_create(); + char* backup_argv[] = {"fastsync", "--backup-dir=/tmp/bak", "/src", "/dst"}; + positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 4, backup_argv, positional_args, &positional_count), 0); + EXPECT_EQ_STR(cfg->backup_dir, "/tmp/bak"); + config_delete(cfg); +} + +/* Options that take a separate value must report "missing argument", not the + * generic "Unknown option", when they are the final argv entry. */ +static void test_parse_args_missing_argument_diagnostic() { + static const char* const options[] = {"--exclude", "--server-port", "--skip-compress", "-T"}; + + for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { + Config* cfg = config_create(); + char* argv[] = {"fastsync", (char*)options[i]}; + int positional_args[2]; + int positional_count = 0; + FILE* log_file = tmpfile(); + char log_buffer[512] = {0}; + + EXPECT_NOT_NULL(log_file); + log_set_file(log_file); + + EXPECT_EQ_INT(parse_args(cfg, 2, argv, positional_args, &positional_count), -1); + fflush(log_file); + rewind(log_file); + EXPECT_TRUE(fread(log_buffer, 1, sizeof(log_buffer) - 1, log_file) > 0); + EXPECT_TRUE(strstr(log_buffer, "missing argument") != NULL); + EXPECT_TRUE(strstr(log_buffer, "Unknown option") == NULL); + + log_set_file(NULL); + fclose(log_file); + config_delete(cfg); + } +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -1155,6 +1219,9 @@ void test_client_cli() { test_parse_args_compression_alias_equals(); test_parse_args_rejects_invalid_compression_level_equals(); test_parse_args_rejects_invalid_compression_choice(); + test_parse_args_table_equals_size_options(); + test_parse_args_table_equals_string_and_int_options(); + test_parse_args_missing_argument_diagnostic(); test_parse_args_partial_progress(); test_parse_args_checksum_choice_aliases(); test_parse_args_checksum_choice_requires_value();