diff --git a/README.md b/README.md index cabb909..9c35016 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,9 @@ replacement for every rsync feature or protocol mode. - `--partial`, `--partial-dir`, `-P`, `--append`, and `--append-verify` are not yet full rsync-style resumable transfers. Interrupted files are not retained for resumption. +- `--dirs` is not implemented. Its compatibility aliases `--old-dirs` and + `--old-d` are recognized but rejected explicitly rather than silently using + FastSync's recursive directory behavior. - Several rsync short options currently have FastSync-specific meanings. Do not assume every short option is interchangeable yet. diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 6204d3f..cf03c4e 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -72,7 +72,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `-r`, `--recursive` | Recurse into directories | ✅ Implemented | Default behavior | | `-R`, `--relative` | Use relative path names | ❌ Not Implemented | Removed because it had no effect | | `--no-implied-dirs` | Don't send implied dirs with -R | ❌ Not Implemented | | -| `-d`, `--dirs` | Transfer dirs without recursing | ❌ Not Implemented | | +| `-d`, `--dirs`, `--old-dirs`, `--old-d` | Transfer dirs without recursing | ❌ Not Implemented | The aliases are recognized and rejected explicitly; they depend on the unimplemented `--dirs` behavior | | `--mkpath` | Create missing path components | ❌ Not Implemented | | ## 5. Transfer Modifications diff --git a/src/client/client_cli.c b/src/client/client_cli.c index c1e656c..de4b859 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -335,6 +335,7 @@ typedef enum { OPT_POS_INT, OPT_NONNEG_INT, OPT_ULL, + OPT_UNSUPPORTED, } OptKind; typedef struct { @@ -384,6 +385,8 @@ static const OptionEntry OPTION_TABLE[] = { {"--existing", NULL, OPT_FLAG, offsetof(Config, existing)}, {"--ignore-existing", NULL, OPT_FLAG, offsetof(Config, ignore_existing)}, {"--chmod", NULL, OPT_STRING, offsetof(Config, chmod_spec)}, + {"--dirs", "--old-dirs", OPT_UNSUPPORTED, 0}, + {"--old-d", NULL, OPT_UNSUPPORTED, 0}, {"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)}, {"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)}, @@ -491,10 +494,10 @@ static int apply_negation(Config* config, const char* arg) { return 0; } -static int apply_table_option(Config* config, const OptionEntry* entry, const char* value) { +static int apply_table_option(Config* config, const OptionEntry* entry, const char* option_name, + const char* value) { if (entry->kind == OPT_NOOP) return 0; - void* field = (char*)config + entry->offset; switch (entry->kind) { case OPT_FLAG: @@ -517,6 +520,11 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch *(unsigned long long*)field = v; return 0; } + case OPT_UNSUPPORTED: + log_message(LOG_LEVEL_ERROR, + "%s: directory-only transfer is not implemented; refusing to ignore option", + option_name); + return -1; } return -1; } @@ -592,19 +600,23 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, if (!entry) entry = find_table_option_with_equals(argv[i], &inline_value); if (entry) { + const char* option_name = argv[i]; + const char* value = NULL; if (entry->kind != OPT_FLAG) { - const char* value = inline_value; - if (!value && i + 1 < argc) - value = argv[++i]; - if (!value) { - log_message(LOG_LEVEL_ERROR, "missing argument for %s", entry->name); - return -1; + if (entry->kind != OPT_UNSUPPORTED) { + value = inline_value; + if (!value && i + 1 < argc) + value = argv[++i]; + if (!value) { + log_message(LOG_LEVEL_ERROR, "missing argument for %s", entry->name); + return -1; + } } if (strcmp(entry->name, "--compress-choice") == 0) { if (set_compression_choice(config, value) != 0) return -1; } else { - if (apply_table_option(config, entry, value) != 0) + if (apply_table_option(config, entry, option_name, value) != 0) return -1; if (strcmp(entry->name, "--compress-level") == 0 && (config->compression_level < 1 || config->compression_level > 22)) { @@ -620,7 +632,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, config->use_metadata = true; } } - } else if (apply_table_option(config, entry, NULL) != 0) { + } else if (apply_table_option(config, entry, option_name, NULL) != 0) { return -1; } if (entry->offset == offsetof(Config, eight_bit_output)) @@ -839,7 +851,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } } } -set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING)); + set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING)); if (config->compress_choice) config->use_compression = strcmp(config->compress_choice, "zstd") == 0; diff --git a/src/client/usage.c b/src/client/usage.c index 7aa0b3d..35116f7 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -25,6 +25,8 @@ void print_usage(void) { printf(" -8, --8-bit-output Leave high-bit characters unescaped in output\n"); printf(" --delete Delete files on receiver not in source\n"); printf(" --ignore-existing Skip files that already exist on receiver\n"); + printf( + " --dirs, --old-dirs, --old-d Transfer directories without recursing (not implemented)\n"); printf(" --exclude Exclude files matching pattern\n"); printf(" --include Only include files matching pattern\n"); printf(" --exclude-from Read exclude patterns from file\n"); @@ -96,7 +98,7 @@ void print_usage(void) { printf(" --inplace Update files in-place (no temp+rename)\n"); printf(" --fsync Fsync every written file before publication\n"); printf(" --compress-level Compression level (default: 5)\n"); -printf(" --zl Alias for --compress-level\n"); + printf(" --zl Alias for --compress-level\n"); printf(" --skip-compress=LIST Skip compression for comma-separated suffixes\n"); printf(" --compress-threads Compression worker threads (requires zstd threaded support)\n"); printf(" --no-OPTION Disable a supported boolean option\n"); diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index e9d73ca..7c631ad 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -546,6 +546,34 @@ static void test_parse_args_unknown_option() { config_delete(cfg); } +/* Directory aliases must report the unsupported directory-only behavior clearly. */ +static void test_parse_args_rejects_dirs_aliases() { + static const char* const options[] = {"--dirs", "--old-dirs", "--old-d"}; + + for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { + Config* cfg = config_create(); + char* argv[] = {"fastsync", (char*)options[i], "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + FILE* log_file = tmpfile(); + char log_buffer[256] = {0}; + + EXPECT_NOT_NULL(log_file); + log_set_file(log_file); + + EXPECT_EQ_INT(parse_args(cfg, 4, 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, options[i]) != NULL); + EXPECT_TRUE(strstr(log_buffer, "directory-only transfer is not implemented") != NULL); + EXPECT_TRUE(strstr(log_buffer, "requires --dirs") == NULL); + log_set_file(NULL); + fclose(log_file); + config_delete(cfg); + } +} + /* Parsed-but-unimplemented options must fail instead of being silently accepted. */ static void test_parse_args_rejects_unimplemented_options() { static const char* const options[] = {"--silent", @@ -1081,6 +1109,7 @@ void test_client_cli() { test_parse_args_max_alloc_sizes(); test_parse_args_rejects_invalid_max_alloc(); test_parse_args_unknown_option(); + test_parse_args_rejects_dirs_aliases(); test_parse_args_rejects_unimplemented_options(); test_parse_args_quiet(); test_parse_args_human_readable();