feat(cli): rsync short-option clustering and inline/attached values (#285)
Implement rsync 3.4.1 client-CLI parity: - cluster boolean shorts (-av, -aAX, -rlpt) and accept attached values (-B1048576, -essh, -Mfoo); add the -r, -b, -L and -B short aliases (-r is a faithful no-op since FastSync is always recursive) - stop OPT_NOOP (-s/--secluded-args, -r/--recursive) from swallowing the next argv - add inline --opt=value for every value-taking long option, including --exclude/--include/--exclude-from/--include-from/--log-file (#291) - accept --port on the server CLI in addition to -p (#296) - reject unknown flags naming the flag and stating it is unsupported Unit tests cover clustering, attached/inline values, the OPT_NOOP argument-consumption fix and rejected shorts.
This commit is contained in:
+287
-83
@@ -647,17 +647,20 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
{"--save-to-disk", NULL, OPT_FLAG, offsetof(Config, save_to_disk)},
|
{"--save-to-disk", NULL, OPT_FLAG, offsetof(Config, save_to_disk)},
|
||||||
{"--progress", NULL, OPT_FLAG, offsetof(Config, show_progress)},
|
{"--progress", NULL, OPT_FLAG, offsetof(Config, show_progress)},
|
||||||
{"--tls", NULL, OPT_FLAG, offsetof(Config, use_tls)},
|
{"--tls", NULL, OPT_FLAG, offsetof(Config, use_tls)},
|
||||||
{"--backup", NULL, OPT_FLAG, offsetof(Config, backup)},
|
{"--backup", "-b", OPT_FLAG, offsetof(Config, backup)},
|
||||||
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
|
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
|
||||||
{"--human-readable", "-h", OPT_FLAG, offsetof(Config, human_readable)},
|
{"--human-readable", "-h", OPT_FLAG, offsetof(Config, human_readable)},
|
||||||
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
|
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
|
||||||
{"--secluded-args", "-s", OPT_NOOP, 0},
|
{"--secluded-args", "-s", OPT_NOOP, 0},
|
||||||
|
/* rsync -r/--recursive: FastSync is always recursive, so this is a
|
||||||
|
* faithful no-op (accepted silently, never consumes an argument). */
|
||||||
|
{"--recursive", "-r", OPT_NOOP, 0},
|
||||||
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
|
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
|
||||||
{"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)},
|
{"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)},
|
||||||
{"--rsh", "-e", OPT_STRING, offsetof(Config, rsh_command)},
|
{"--rsh", "-e", OPT_STRING, offsetof(Config, rsh_command)},
|
||||||
{"--blocking-io", NULL, OPT_FLAG, offsetof(Config, blocking_io)},
|
{"--blocking-io", NULL, OPT_FLAG, offsetof(Config, blocking_io)},
|
||||||
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
|
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
|
||||||
{"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)},
|
{"--copy-links", "-L", OPT_FLAG, offsetof(Config, copy_links)},
|
||||||
{"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)},
|
{"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)},
|
||||||
{"--copy-unsafe-links", NULL, OPT_FLAG, offsetof(Config, copy_unsafe_links)},
|
{"--copy-unsafe-links", NULL, OPT_FLAG, offsetof(Config, copy_unsafe_links)},
|
||||||
{"--copy-dirlinks", "-k", OPT_FLAG, offsetof(Config, copy_dirlinks)},
|
{"--copy-dirlinks", "-k", OPT_FLAG, offsetof(Config, copy_dirlinks)},
|
||||||
@@ -1108,7 +1111,7 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
|||||||
if (!entry)
|
if (!entry)
|
||||||
return false;
|
return false;
|
||||||
const char* value = NULL;
|
const char* value = NULL;
|
||||||
if (entry->kind != OPT_FLAG) {
|
if (entry->kind != OPT_FLAG && entry->kind != OPT_NOOP) {
|
||||||
value = inline_value;
|
value = inline_value;
|
||||||
if (!value && ctx->i + 1 < ctx->argc)
|
if (!value && ctx->i + 1 < ctx->argc)
|
||||||
value = ctx->argv[++ctx->i];
|
value = ctx->argv[++ctx->i];
|
||||||
@@ -1266,6 +1269,26 @@ static bool cli_handle_meta_flags(CliParseCtx* ctx) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Apply a --delta-max value. Values below the minimum warn and keep the
|
||||||
|
* default; values above the maximum are a hard error. Returns 0 on success,
|
||||||
|
* -1 on error. */
|
||||||
|
static int set_delta_max_option(Config* config, const char* value) {
|
||||||
|
unsigned long long val;
|
||||||
|
if (parse_ull_arg(value, &val, "--delta-max") != 0)
|
||||||
|
return -1;
|
||||||
|
if (val >= DELTA_MIN_FILE_SIZE && val <= DELTA_MAX_FILE_SIZE) {
|
||||||
|
config->delta_max_file_size = val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (val < DELTA_MIN_FILE_SIZE) {
|
||||||
|
log_message(LOG_LEVEL_WARNING, "--delta-max value %llu too small, using default", val);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--delta-max must not exceed %llu bytes",
|
||||||
|
(unsigned long long)DELTA_MAX_FILE_SIZE);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* SSH port and pattern/block-size options. Returns true when the argument was
|
/* SSH port and pattern/block-size options. Returns true when the argument was
|
||||||
* consumed. */
|
* consumed. */
|
||||||
static bool cli_handle_ssh_and_pattern_options(CliParseCtx* ctx) {
|
static bool cli_handle_ssh_and_pattern_options(CliParseCtx* ctx) {
|
||||||
@@ -1298,6 +1321,12 @@ static bool cli_handle_ssh_and_pattern_options(CliParseCtx* ctx) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (strncmp(arg, "--exclude=", 10) == 0) {
|
||||||
|
if (config_add_pattern(&config->exclude_patterns, &config->exclude_count, arg + 10,
|
||||||
|
"--exclude") != 0)
|
||||||
|
ctx->exit_code = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (opt_is(arg, "--exclude", NULL)) {
|
if (opt_is(arg, "--exclude", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
@@ -1309,6 +1338,12 @@ static bool cli_handle_ssh_and_pattern_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (strncmp(arg, "--include=", 10) == 0) {
|
||||||
|
if (config_add_pattern(&config->include_patterns, &config->include_count, arg + 10,
|
||||||
|
"--include") != 0)
|
||||||
|
ctx->exit_code = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (opt_is(arg, "--include", NULL)) {
|
if (opt_is(arg, "--include", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
@@ -1330,7 +1365,8 @@ static bool cli_handle_ssh_and_pattern_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (opt_is(arg, "--delta-block", "--block-size")) {
|
if (strcmp(arg, "--delta-block") == 0 || strcmp(arg, "--block-size") == 0 ||
|
||||||
|
strcmp(arg, "-B") == 0) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
@@ -1340,28 +1376,21 @@ static bool cli_handle_ssh_and_pattern_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (strncmp(arg, "--delta-max=", 12) == 0) {
|
||||||
|
if (set_delta_max_option(config, arg + 12) != 0)
|
||||||
|
ctx->exit_code = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (opt_is(arg, "--delta-max", NULL)) {
|
if (opt_is(arg, "--delta-max", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
unsigned long long val;
|
if (set_delta_max_option(config, ctx->argv[++ctx->i]) != 0)
|
||||||
if (parse_ull_arg(ctx->argv[++ctx->i], &val, "--delta-max") != 0) {
|
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (val >= DELTA_MIN_FILE_SIZE && val <= DELTA_MAX_FILE_SIZE) {
|
|
||||||
config->delta_max_file_size = val;
|
|
||||||
} else if (val < DELTA_MIN_FILE_SIZE) {
|
|
||||||
log_message(LOG_LEVEL_WARNING, "--delta-max value %llu too small, using default", val);
|
|
||||||
} else {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "--delta-max must not exceed %llu bytes",
|
|
||||||
(unsigned long long)DELTA_MAX_FILE_SIZE);
|
|
||||||
ctx->exit_code = -1;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,6 +1480,73 @@ static int set_server_port_option(Config* config, const char* value, const char*
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Open (create/append) a --log-file target and install it in the logger.
|
||||||
|
* Refuses a symlinked target and never leaks the descriptor across exec: an
|
||||||
|
* attacker who can plant a symlink in the working directory must not be able to
|
||||||
|
* redirect (or truncate) an arbitrary file. The log is created with owner-only
|
||||||
|
* permissions. Returns 0 on success, -1 on error (already logged). */
|
||||||
|
static int set_log_file_option(Config* config, const char* log_path) {
|
||||||
|
if (config->log_file) {
|
||||||
|
/* Detach the logger before closing: log I/O may be in flight and must
|
||||||
|
never touch a freed FILE*. */
|
||||||
|
log_set_file(NULL);
|
||||||
|
fclose(config->log_file);
|
||||||
|
config->log_file = NULL;
|
||||||
|
}
|
||||||
|
int log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||||
|
FILE* lf = log_fd >= 0 ? fdopen(log_fd, "a") : NULL;
|
||||||
|
if (!lf) {
|
||||||
|
int open_errno = errno;
|
||||||
|
if (log_fd >= 0)
|
||||||
|
close(log_fd);
|
||||||
|
char* escaped = output_escape(log_path, false);
|
||||||
|
log_message(LOG_LEVEL_ERROR, "could not open log file '%s': %s",
|
||||||
|
escaped ? escaped : "<allocation failed>", strerror(open_errno));
|
||||||
|
free(escaped);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
config->log_file = lf;
|
||||||
|
log_set_file(lf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply a --bwlimit value (kilobytes per second). Returns 0 on success, -1 on
|
||||||
|
* error. */
|
||||||
|
static int set_bwlimit_option(const char* value) {
|
||||||
|
unsigned long long kbps;
|
||||||
|
if (parse_ull_arg(value, &kbps, "--bwlimit") != 0)
|
||||||
|
return -1;
|
||||||
|
if (kbps == 0) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--bwlimit must be a positive integer");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (kbps > ULLONG_MAX / 1024) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--bwlimit value too large");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
io_set_bwlimit(kbps * 1024);
|
||||||
|
log_info_message(LOG_INFO_MISC, "Set bandwidth limit to %llu KB/s", kbps);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply a --chunk-size value. Returns 0 on success, -1 on error. */
|
||||||
|
static int set_chunk_size_option(Config* config, const char* value) {
|
||||||
|
unsigned long long val;
|
||||||
|
if (parse_ull_arg(value, &val, "--chunk-size") != 0)
|
||||||
|
return -1;
|
||||||
|
if (val == 0) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--chunk-size must be a positive integer");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (val > MAX_CHUNK_SIZE) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--chunk-size must be between 1 and %llu",
|
||||||
|
(unsigned long long)MAX_CHUNK_SIZE);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
config->chunk_size = val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Network/IO options: --server-port/--port, --bwlimit, --chunk-size, --log-file
|
/* Network/IO options: --server-port/--port, --bwlimit, --chunk-size, --log-file
|
||||||
* and --stderr. Returns true when the argument was consumed. */
|
* and --stderr. Returns true when the argument was consumed. */
|
||||||
static bool cli_handle_io_options(CliParseCtx* ctx) {
|
static bool cli_handle_io_options(CliParseCtx* ctx) {
|
||||||
@@ -1475,91 +1571,51 @@ static bool cli_handle_io_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (strncmp(arg, "--bwlimit=", 10) == 0) {
|
||||||
|
if (set_bwlimit_option(arg + 10) != 0)
|
||||||
|
ctx->exit_code = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (opt_is(arg, "--bwlimit", NULL)) {
|
if (opt_is(arg, "--bwlimit", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
unsigned long long kbps;
|
if (set_bwlimit_option(ctx->argv[++ctx->i]) != 0)
|
||||||
if (parse_ull_arg(ctx->argv[++ctx->i], &kbps, "--bwlimit") != 0) {
|
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (kbps == 0) {
|
if (strncmp(arg, "--chunk-size=", 13) == 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "--bwlimit must be a positive integer");
|
if (set_chunk_size_option(config, arg + 13) != 0)
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (kbps > ULLONG_MAX / 1024) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "--bwlimit value too large");
|
|
||||||
ctx->exit_code = -1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
io_set_bwlimit(kbps * 1024);
|
|
||||||
log_info_message(LOG_INFO_MISC, "Set bandwidth limit to %llu KB/s", kbps);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (opt_is(arg, "--chunk-size", NULL)) {
|
if (opt_is(arg, "--chunk-size", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
unsigned long long val;
|
if (set_chunk_size_option(config, ctx->argv[++ctx->i]) != 0)
|
||||||
if (parse_ull_arg(ctx->argv[++ctx->i], &val, "--chunk-size") != 0) {
|
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (val == 0) {
|
if (strncmp(arg, "--log-file=", 11) == 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "--chunk-size must be a positive integer");
|
if (set_log_file_option(config, arg + 11) != 0)
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (val > MAX_CHUNK_SIZE) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "--chunk-size must be between 1 and %llu",
|
|
||||||
(unsigned long long)MAX_CHUNK_SIZE);
|
|
||||||
ctx->exit_code = -1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
config->chunk_size = val;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (opt_is(arg, "--log-file", NULL)) {
|
if (opt_is(arg, "--log-file", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (config->log_file) {
|
if (set_log_file_option(config, ctx->argv[++ctx->i]) != 0)
|
||||||
/* Detach the logger before closing: log I/O may be in flight and must
|
|
||||||
never touch a freed FILE*. */
|
|
||||||
log_set_file(NULL);
|
|
||||||
fclose(config->log_file);
|
|
||||||
config->log_file = NULL;
|
|
||||||
}
|
|
||||||
const char* log_path = ctx->argv[++ctx->i];
|
|
||||||
/* Refuse a symlinked target and never leak the descriptor across exec: an
|
|
||||||
* attacker who can plant a symlink in the working directory must not be
|
|
||||||
* able to redirect (or truncate) an arbitrary file via --log-file. The log
|
|
||||||
* is created with owner-only permissions. */
|
|
||||||
int log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW | O_CLOEXEC, 0600);
|
|
||||||
FILE* lf = log_fd >= 0 ? fdopen(log_fd, "a") : NULL;
|
|
||||||
if (!lf) {
|
|
||||||
int open_errno = errno;
|
|
||||||
if (log_fd >= 0)
|
|
||||||
close(log_fd);
|
|
||||||
char* escaped = output_escape(log_path, false);
|
|
||||||
log_message(LOG_LEVEL_ERROR, "could not open log file '%s': %s",
|
|
||||||
escaped ? escaped : "<allocation failed>", strerror(open_errno));
|
|
||||||
free(escaped);
|
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
config->log_file = lf;
|
|
||||||
log_set_file(lf);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (strncmp(arg, "--stderr=", 9) == 0) {
|
if (strncmp(arg, "--stderr=", 9) == 0) {
|
||||||
if (set_stderr_mode(arg + 9) != 0)
|
if (set_stderr_mode(arg + 9) != 0)
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
@@ -1577,6 +1633,11 @@ static bool cli_handle_io_options(CliParseCtx* ctx) {
|
|||||||
static bool cli_handle_filter_options(CliParseCtx* ctx) {
|
static bool cli_handle_filter_options(CliParseCtx* ctx) {
|
||||||
Config* config = ctx->config;
|
Config* config = ctx->config;
|
||||||
const char* arg = ctx->argv[ctx->i];
|
const char* arg = ctx->argv[ctx->i];
|
||||||
|
if (strncmp(arg, "--exclude-from=", 15) == 0) {
|
||||||
|
if (read_patterns_from_file(arg + 15, &config->exclude_patterns, &config->exclude_count) != 0)
|
||||||
|
ctx->exit_code = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (opt_is(arg, "--exclude-from", NULL)) {
|
if (opt_is(arg, "--exclude-from", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
@@ -1588,6 +1649,11 @@ static bool cli_handle_filter_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (strncmp(arg, "--include-from=", 15) == 0) {
|
||||||
|
if (read_patterns_from_file(arg + 15, &config->include_patterns, &config->include_count) != 0)
|
||||||
|
ctx->exit_code = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (opt_is(arg, "--include-from", NULL)) {
|
if (opt_is(arg, "--include-from", NULL)) {
|
||||||
if (ctx->i + 1 >= ctx->argc) {
|
if (ctx->i + 1 >= ctx->argc) {
|
||||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
log_message(LOG_LEVEL_ERROR, "missing argument for %s", arg);
|
||||||
@@ -2025,18 +2091,149 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* True for the rsync short options that take a value: the remainder of the
|
||||||
|
* cluster is the value (attached form), or the next argv entry when the option
|
||||||
|
* is written alone. */
|
||||||
|
static bool short_takes_value(char c) {
|
||||||
|
return c == 'e' || c == 'B' || c == 'M' || c == 'f' || c == 'T' || c == '@';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* True when the long option consumes the following argv entry as its value
|
||||||
|
* (i.e. a value-taking option written without an inline "="). The cluster
|
||||||
|
* expander consults this so a value that happens to start with '-' (e.g.
|
||||||
|
* --filter "- *.tmp") is copied verbatim instead of being mistaken for a
|
||||||
|
* short-option cluster. */
|
||||||
|
static bool cli_long_takes_separate_value(const char* arg) {
|
||||||
|
if (strchr(arg, '='))
|
||||||
|
return false;
|
||||||
|
const OptionEntry* entry = find_table_option(arg);
|
||||||
|
if (entry)
|
||||||
|
return entry->kind == OPT_STRING || entry->kind == OPT_POS_INT ||
|
||||||
|
entry->kind == OPT_NONNEG_INT || entry->kind == OPT_ULL;
|
||||||
|
static const char* const extra[] = {
|
||||||
|
"--ssh-port", "--exclude", "--include", "--exclude-from",
|
||||||
|
"--include-from", "--files-from", "--filter", "--delta-block",
|
||||||
|
"--block-size", "--delta-max", "--server-port", "--port",
|
||||||
|
"--bwlimit", "--chunk-size", "--log-file", "--stderr",
|
||||||
|
"--stop-after", "--stop-at", "--max-alloc", "--compress-threads",
|
||||||
|
"--checksum-choice", "--cc", "--checksum-seed", "--sockopts",
|
||||||
|
"--remote-option", "--compare-dest", "--copy-dest", "--link-dest",
|
||||||
|
"--usermap", "--groupmap", "--chown", "--copy-as",
|
||||||
|
"--outbuf", "--debug", "--info",
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(extra) / sizeof(extra[0]); i++)
|
||||||
|
if (strcmp(arg, extra[i]) == 0)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expand rsync-style short-option clusters into one option per token before
|
||||||
|
* parsing: -av -> -a -v, -rlpt -> -r -l -p -t, -B1048576 -> -B 1048576 and
|
||||||
|
* -essh -> -e ssh. A value-taking short consumes the remainder of its token
|
||||||
|
* (an optional leading '=' is dropped) as its value; otherwise a value-taking
|
||||||
|
* option written alone takes the next argv entry, which is therefore copied
|
||||||
|
* verbatim. Every expanded token is a copy; *out_orig maps each expanded
|
||||||
|
* token back to its source argv index so the positional-argument indices
|
||||||
|
* returned to main() stay valid for the caller's original argv. Returns 0 on
|
||||||
|
* success, -1 on allocation failure. */
|
||||||
|
static int expand_short_clusters(int argc, char* argv[], char*** out_argv, int** out_orig,
|
||||||
|
int* out_argc) {
|
||||||
|
size_t cap = 1;
|
||||||
|
for (int i = 0; i < argc; i++)
|
||||||
|
cap += strlen(argv[i]) + 2;
|
||||||
|
char** exp = calloc(cap, sizeof(char*));
|
||||||
|
int* orig = calloc(cap, sizeof(int));
|
||||||
|
if (!exp || !orig) {
|
||||||
|
free(exp);
|
||||||
|
free(orig);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int n = 0;
|
||||||
|
bool expect_value = false;
|
||||||
|
for (int i = 0; i < argc; i++) {
|
||||||
|
const char* tok = argv[i];
|
||||||
|
if (i == 0 || expect_value || tok[0] != '-' || tok[1] == '\0') {
|
||||||
|
exp[n] = str_dup(tok);
|
||||||
|
if (!exp[n])
|
||||||
|
goto oom;
|
||||||
|
orig[n] = i;
|
||||||
|
n++;
|
||||||
|
expect_value = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tok[1] == '-') {
|
||||||
|
exp[n] = str_dup(tok);
|
||||||
|
if (!exp[n])
|
||||||
|
goto oom;
|
||||||
|
orig[n] = i;
|
||||||
|
n++;
|
||||||
|
expect_value = cli_long_takes_separate_value(tok);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t len = strlen(tok);
|
||||||
|
for (size_t j = 1; j < len; j++) {
|
||||||
|
char flag[3] = {'-', tok[j], '\0'};
|
||||||
|
exp[n] = str_dup(flag);
|
||||||
|
if (!exp[n])
|
||||||
|
goto oom;
|
||||||
|
orig[n] = i;
|
||||||
|
n++;
|
||||||
|
if (short_takes_value(tok[j])) {
|
||||||
|
const char* value = tok + j + 1;
|
||||||
|
if (*value == '=')
|
||||||
|
value++;
|
||||||
|
if (*value != '\0') {
|
||||||
|
exp[n] = str_dup(value);
|
||||||
|
if (!exp[n])
|
||||||
|
goto oom;
|
||||||
|
orig[n] = i;
|
||||||
|
n++;
|
||||||
|
} else {
|
||||||
|
expect_value = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out_argv = exp;
|
||||||
|
*out_orig = orig;
|
||||||
|
*out_argc = n;
|
||||||
|
return 0;
|
||||||
|
oom:
|
||||||
|
for (int k = 0; k < n; k++)
|
||||||
|
free(exp[k]);
|
||||||
|
free(exp);
|
||||||
|
free(orig);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_expanded_args(char** exp, int exp_argc) {
|
||||||
|
for (int i = 0; i < exp_argc; i++)
|
||||||
|
free(exp[i]);
|
||||||
|
free(exp);
|
||||||
|
}
|
||||||
|
|
||||||
/* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */
|
/* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */
|
||||||
int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||||
int* positional_count) {
|
int* positional_count) {
|
||||||
protocol_set_8_bit_output(config->eight_bit_output);
|
protocol_set_8_bit_output(config->eight_bit_output);
|
||||||
|
|
||||||
if (cli_apply_output_controls(config, argc, argv) != 0)
|
char** exp_argv = NULL;
|
||||||
|
int* exp_orig = NULL;
|
||||||
|
int exp_argc = 0;
|
||||||
|
if (expand_short_clusters(argc, argv, &exp_argv, &exp_orig, &exp_argc) != 0) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "memory allocation failed parsing arguments");
|
||||||
return -1;
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = -1;
|
||||||
|
if (cli_apply_output_controls(config, exp_argc, exp_argv) != 0)
|
||||||
|
goto done;
|
||||||
|
|
||||||
CliParseCtx ctx = {
|
CliParseCtx ctx = {
|
||||||
.config = config,
|
.config = config,
|
||||||
.argc = argc,
|
.argc = exp_argc,
|
||||||
.argv = argv,
|
.argv = exp_argv,
|
||||||
.i = 1,
|
.i = 1,
|
||||||
.exit_code = 0,
|
.exit_code = 0,
|
||||||
.verbose = false,
|
.verbose = false,
|
||||||
@@ -2044,7 +2241,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
|||||||
.no_incremental = false,
|
.no_incremental = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (ctx.i = 1; ctx.i < argc; ctx.i++) {
|
for (ctx.i = 1; ctx.i < exp_argc; ctx.i++) {
|
||||||
ctx.exit_code = 0;
|
ctx.exit_code = 0;
|
||||||
bool handled = cli_handle_pre_negation(&ctx) || cli_handle_range_time_options(&ctx) ||
|
bool handled = cli_handle_pre_negation(&ctx) || cli_handle_range_time_options(&ctx) ||
|
||||||
cli_handle_table_option(&ctx) || cli_handle_inline_chmod(&ctx) ||
|
cli_handle_table_option(&ctx) || cli_handle_inline_chmod(&ctx) ||
|
||||||
@@ -2054,30 +2251,37 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
|||||||
cli_handle_checksum_options(&ctx) || cli_handle_remote_basis_options(&ctx) ||
|
cli_handle_checksum_options(&ctx) || cli_handle_remote_basis_options(&ctx) ||
|
||||||
cli_handle_outbuf_option(&ctx);
|
cli_handle_outbuf_option(&ctx);
|
||||||
if (handled) {
|
if (handled) {
|
||||||
if (ctx.exit_code != 0)
|
if (ctx.exit_code != 0) {
|
||||||
return ctx.exit_code;
|
result = ctx.exit_code;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argv[ctx.i][0] == '-') {
|
if (ctx.argv[ctx.i][0] == '-') {
|
||||||
char* escaped = output_escape(argv[ctx.i], false);
|
char* escaped = output_escape(ctx.argv[ctx.i], false);
|
||||||
fprintf(stderr, "Unknown option: %s\n", escaped ? escaped : "<allocation failed>");
|
fprintf(stderr, "Unknown option: %s (FastSync does not support this option)\n",
|
||||||
|
escaped ? escaped : "<allocation failed>");
|
||||||
free(escaped);
|
free(escaped);
|
||||||
print_usage();
|
print_usage();
|
||||||
return -1;
|
goto done;
|
||||||
}
|
}
|
||||||
if (*positional_count < 2)
|
if (*positional_count < 2)
|
||||||
positional_args[(*positional_count)++] = ctx.i;
|
positional_args[(*positional_count)++] = exp_orig[ctx.i];
|
||||||
else {
|
else {
|
||||||
char* escaped = output_escape(argv[ctx.i], false);
|
char* escaped = output_escape(ctx.argv[ctx.i], false);
|
||||||
fprintf(stderr, "Unexpected argument: %s\n", escaped ? escaped : "<allocation failed>");
|
fprintf(stderr, "Unexpected argument: %s\n", escaped ? escaped : "<allocation failed>");
|
||||||
free(escaped);
|
free(escaped);
|
||||||
print_usage();
|
print_usage();
|
||||||
return -1;
|
goto done;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli_finalize_config(config, ctx.verbose, ctx.no_delta, ctx.no_incremental);
|
result = cli_finalize_config(config, ctx.verbose, ctx.no_delta, ctx.no_incremental);
|
||||||
|
done:
|
||||||
|
free_expanded_args(exp_argv, exp_argc);
|
||||||
|
free(exp_orig);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) {
|
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) {
|
||||||
|
|||||||
+9
-8
@@ -23,6 +23,7 @@ void print_usage(void) {
|
|||||||
printf(" -a, --archive rsync archive mode (-rlptgoD): links, perms, times,\n");
|
printf(" -a, --archive rsync archive mode (-rlptgoD): links, perms, times,\n");
|
||||||
printf(" owner, group, devices and specials; not\n");
|
printf(" owner, group, devices and specials; not\n");
|
||||||
printf(" compression/multithreading\n");
|
printf(" compression/multithreading\n");
|
||||||
|
printf(" -r, --recursive Recurse into directories (FastSync is always recursive)\n");
|
||||||
printf(" -n, --dry-run Show what would be transferred\n");
|
printf(" -n, --dry-run Show what would be transferred\n");
|
||||||
printf(" --remove-source-files Remove regular source files after successful transfer\n");
|
printf(" --remove-source-files Remove regular source files after successful transfer\n");
|
||||||
printf(" -p, --perms Preserve permission bits\n");
|
printf(" -p, --perms Preserve permission bits\n");
|
||||||
@@ -104,10 +105,10 @@ void print_usage(void) {
|
|||||||
printf(" parent directory is not itself listed\n");
|
printf(" parent directory is not itself listed\n");
|
||||||
printf(" --mkpath Create the destination root directory on the server when it\n");
|
printf(" --mkpath Create the destination root directory on the server when it\n");
|
||||||
printf(" does not exist yet\n");
|
printf(" does not exist yet\n");
|
||||||
printf(" --exclude <pattern> Exclude files matching pattern\n");
|
printf(" --exclude <pattern>, --exclude=<pattern> Exclude files matching pattern\n");
|
||||||
printf(" --include <pattern> Only include files matching pattern\n");
|
printf(" --include <pattern>, --include=<pattern> Only include files matching pattern\n");
|
||||||
printf(" --exclude-from <file> Read exclude patterns from file\n");
|
printf(" --exclude-from <file>, --exclude-from=<file> Read exclude patterns from file\n");
|
||||||
printf(" --include-from <file> Read include patterns from file\n");
|
printf(" --include-from <file>, --include-from=<file> Read include patterns from file\n");
|
||||||
printf(" --files-from <file> Read the source file list from FILE (paths relative to the "
|
printf(" --files-from <file> Read the source file list from FILE (paths relative to the "
|
||||||
"source root)\n");
|
"source root)\n");
|
||||||
printf(" -0, --from0 Entries in --files-from are NUL-delimited\n");
|
printf(" -0, --from0 Entries in --files-from are NUL-delimited\n");
|
||||||
@@ -145,7 +146,7 @@ void print_usage(void) {
|
|||||||
printf(" --incremental and --delta; inert with --whole-file,\n");
|
printf(" --incremental and --delta; inert with --whole-file,\n");
|
||||||
printf(" --no-delta, or --no-incremental)\n");
|
printf(" --no-delta, or --no-incremental)\n");
|
||||||
printf(" --no-fuzzy Disable --fuzzy\n");
|
printf(" --no-fuzzy Disable --fuzzy\n");
|
||||||
printf(" --delta-block <n>, --block-size <n>\n");
|
printf(" -B <n>, --block-size <n>, --delta-block <n>\n");
|
||||||
printf(" Delta block size in bytes (default: %d)\n", DELTA_BLOCK_SIZE_DEFAULT);
|
printf(" Delta block size in bytes (default: %d)\n", DELTA_BLOCK_SIZE_DEFAULT);
|
||||||
printf(" --delta-max <n> Max file size for delta transfer (default: %llu)\n",
|
printf(" --delta-max <n> Max file size for delta transfer (default: %llu)\n",
|
||||||
DELTA_MAX_FILE_SIZE);
|
DELTA_MAX_FILE_SIZE);
|
||||||
@@ -246,7 +247,7 @@ void print_usage(void) {
|
|||||||
printf(" -6, --ipv6 Force IPv6 for destination resolution\n");
|
printf(" -6, --ipv6 Force IPv6 for destination resolution\n");
|
||||||
printf(" --sockopts=OPTS Comma-separated OPT=VAL socket options applied before connect:\n");
|
printf(" --sockopts=OPTS Comma-separated OPT=VAL socket options applied before connect:\n");
|
||||||
printf(" TCP_NODELAY, SO_KEEPALIVE, SO_RCVBUF, SO_SNDBUF, SO_REUSEADDR\n");
|
printf(" TCP_NODELAY, SO_KEEPALIVE, SO_RCVBUF, SO_SNDBUF, SO_REUSEADDR\n");
|
||||||
printf(" --backup Backup existing files before overwriting\n");
|
printf(" -b, --backup Backup existing files before overwriting\n");
|
||||||
printf(" --backup-dir <dir> Directory for backups (requires --backup)\n");
|
printf(" --backup-dir <dir> Directory for backups (requires --backup)\n");
|
||||||
printf(" --suffix <str> Backup suffix (default: ~)\n");
|
printf(" --suffix <str> Backup suffix (default: ~)\n");
|
||||||
printf(" --stats Print transfer statistics at end\n");
|
printf(" --stats Print transfer statistics at end\n");
|
||||||
@@ -257,7 +258,7 @@ void print_usage(void) {
|
|||||||
printf(" -h, --human-readable Print byte sizes in human-readable form\n");
|
printf(" -h, --human-readable Print byte sizes in human-readable form\n");
|
||||||
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
|
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
|
||||||
printf(" -x, --one-file-system Do not cross filesystem boundaries\n");
|
printf(" -x, --one-file-system Do not cross filesystem boundaries\n");
|
||||||
printf(" --log-file <path> Write log messages to file\n");
|
printf(" --log-file <path>, --log-file=<path> Write log messages to file\n");
|
||||||
printf(" --stderr=MODE Route logging to stderr: errors or all\n");
|
printf(" --stderr=MODE Route logging to stderr: errors or all\n");
|
||||||
printf(" --partial Keep partial files on interrupted transfer\n");
|
printf(" --partial Keep partial files on interrupted transfer\n");
|
||||||
printf(" --partial-dir <dir> Directory for partial files\n");
|
printf(" --partial-dir <dir> Directory for partial files\n");
|
||||||
@@ -275,7 +276,7 @@ void print_usage(void) {
|
|||||||
printf(" incoming file list (fewer checks, faster, potentially unsafe).\n");
|
printf(" incoming file list (fewer checks, faster, potentially unsafe).\n");
|
||||||
printf(" Local receiver policy: never sent to the peer, off by default\n");
|
printf(" Local receiver policy: never sent to the peer, off by default\n");
|
||||||
printf(" -l, --links Copy symlinks as symlinks\n");
|
printf(" -l, --links Copy symlinks as symlinks\n");
|
||||||
printf(" --copy-links Transform symlinks into referent files\n");
|
printf(" -L, --copy-links Transform symlinks into referent files\n");
|
||||||
printf(" --safe-links Skip symlinks that point outside transfer tree\n");
|
printf(" --safe-links Skip symlinks that point outside transfer tree\n");
|
||||||
printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n");
|
printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n");
|
||||||
printf(" -k, --copy-dirlinks Transform symlinks to directories into real dirs\n");
|
printf(" -k, --copy-dirlinks Transform symlinks to directories into real dirs\n");
|
||||||
|
|||||||
+1
-1
@@ -1051,7 +1051,7 @@ static void print_server_usage(void) {
|
|||||||
printf(" --early-input=FILE Second credential store layered over\n");
|
printf(" --early-input=FILE Second credential store layered over\n");
|
||||||
printf(" --password-file (same format); usually a secrets-\n");
|
printf(" --password-file (same format); usually a secrets-\n");
|
||||||
printf(" manager/process-substitution file. Requires --daemon\n");
|
printf(" manager/process-substitution file. Requires --daemon\n");
|
||||||
printf(" -p <port> TCP port (default: 8080, range: 1-65535)\n");
|
printf(" -p, --port <port> TCP port (default: 8080, range: 1-65535)\n");
|
||||||
printf(" --tls Enable TLS encryption\n");
|
printf(" --tls Enable TLS encryption\n");
|
||||||
printf(" --cert <path> TLS certificate file (PEM)\n");
|
printf(" --cert <path> TLS certificate file (PEM)\n");
|
||||||
printf(" --key <path> TLS private key file (PEM)\n");
|
printf(" --key <path> TLS private key file (PEM)\n");
|
||||||
|
|||||||
@@ -192,14 +192,20 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
|||||||
inline_value = argv[++i];
|
inline_value = argv[++i];
|
||||||
}
|
}
|
||||||
opts->iconv_spec = inline_value;
|
opts->iconv_spec = inline_value;
|
||||||
} else if (arg_is(argv[i], "-p")) {
|
} else if (arg_is(argv[i], "-p") || arg_has_value(argv[i], "--port", &inline_value)) {
|
||||||
|
if (inline_value) {
|
||||||
|
opts->port_set = true;
|
||||||
|
if (parse_port_arg(inline_value, &opts->port, err, err_size) != 0)
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
if (i + 1 >= argc) {
|
if (i + 1 >= argc) {
|
||||||
set_error(err, err_size, "missing argument for -p");
|
set_error(err, err_size, "missing argument for %s", argv[i]);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
opts->port_set = true;
|
opts->port_set = true;
|
||||||
if (parse_port_arg(argv[++i], &opts->port, err, err_size) != 0)
|
if (parse_port_arg(argv[++i], &opts->port, err, err_size) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (arg_has_value(argv[i], "--config", &inline_value)) {
|
if (arg_has_value(argv[i], "--config", &inline_value)) {
|
||||||
if (!inline_value) {
|
if (!inline_value) {
|
||||||
|
|||||||
@@ -3632,6 +3632,205 @@ static void test_parse_args_acls_implies_perms_xattrs_does_not() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* rsync short-option clustering: boolean shorts bundle after one dash
|
||||||
|
* (-av == -a -v, -aAX, -rlpt), including the -r recursive no-op. */
|
||||||
|
static void test_parse_args_short_clustering() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
char* argv[] = {"fastsync", "-av", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->follow_symlinks);
|
||||||
|
EXPECT_TRUE(cfg->preserve_perms);
|
||||||
|
EXPECT_TRUE(cfg->preserve_times);
|
||||||
|
EXPECT_TRUE(cfg->preserve_owner);
|
||||||
|
EXPECT_TRUE(cfg->preserve_group);
|
||||||
|
EXPECT_TRUE(cfg->preserve_devices);
|
||||||
|
EXPECT_TRUE(cfg->preserve_specials);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_ax[] = {"fastsync", "-aAX", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_ax, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->follow_symlinks);
|
||||||
|
EXPECT_TRUE(cfg->preserve_acls);
|
||||||
|
EXPECT_TRUE(cfg->preserve_xattrs);
|
||||||
|
EXPECT_TRUE(cfg->preserve_perms);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_rlpt[] = {"fastsync", "-rlpt", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_rlpt, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->follow_symlinks);
|
||||||
|
EXPECT_TRUE(cfg->preserve_perms);
|
||||||
|
EXPECT_TRUE(cfg->preserve_times);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attached short-option values: a value-taking short consumes the remainder of
|
||||||
|
* its token as the value (-B32768, -essh, -Mfoo, -B=... also tolerated). */
|
||||||
|
static void test_parse_args_attached_short_values() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
|
||||||
|
char* argv_b[] = {"fastsync", "-B32768", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_b, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT((int)cfg->delta_block_size, 32768);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
/* An oversized block size parses (and warns) but keeps the default, exactly
|
||||||
|
* like the long --block-size form. */
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_big[] = {"fastsync", "-B1048576", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_big, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT((int)cfg->delta_block_size, (int)DELTA_BLOCK_SIZE_DEFAULT);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
/* A separate value still works for a short written alone. */
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_sep[] = {"fastsync", "-B", "8192", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv_sep, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT((int)cfg->delta_block_size, 8192);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_e[] = {"fastsync", "-essh", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_e, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_STR(cfg->rsh_command, "ssh");
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = valid_client_config();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_m[] = {"fastsync", "-Mfoo=bar", "--source-dir", "/src", "--dest-dir", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 7, argv_m, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT(cfg->remote_option_count, 1);
|
||||||
|
EXPECT_EQ_STR(cfg->remote_options[0], "foo=bar");
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline --opt=value forms for options that previously only accepted a
|
||||||
|
* separate argument. */
|
||||||
|
static void test_parse_args_inline_equals_forms() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
char* argv[] = {"fastsync", "--exclude=*.log", "--include=*.txt", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT(cfg->exclude_count, 1);
|
||||||
|
EXPECT_EQ_STR(cfg->exclude_patterns[0], "*.log");
|
||||||
|
EXPECT_EQ_INT(cfg->include_count, 1);
|
||||||
|
EXPECT_EQ_STR(cfg->include_patterns[0], "*.txt");
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
const char* list_path = "cli_inline_patterns.txt";
|
||||||
|
write_file_bytes(list_path, "*.o\nbuild/\n", 11);
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char arg_excl[64];
|
||||||
|
snprintf(arg_excl, sizeof(arg_excl), "--exclude-from=%s", list_path);
|
||||||
|
char arg_incl[64];
|
||||||
|
snprintf(arg_incl, sizeof(arg_incl), "--include-from=%s", list_path);
|
||||||
|
char* argv2[] = {"fastsync", arg_excl, arg_incl, "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT(cfg->exclude_count, 2);
|
||||||
|
EXPECT_EQ_STR(cfg->exclude_patterns[0], "*.o");
|
||||||
|
EXPECT_EQ_STR(cfg->exclude_patterns[1], "build/");
|
||||||
|
EXPECT_EQ_INT(cfg->include_count, 2);
|
||||||
|
remove(list_path);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
const char* log_path = "cli_inline_log.txt";
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char arg_log[64];
|
||||||
|
snprintf(arg_log, sizeof(arg_log), "--log-file=%s", log_path);
|
||||||
|
char* argv3[] = {"fastsync", arg_log, "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv3, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_NOT_NULL(cfg->log_file);
|
||||||
|
config_delete(cfg);
|
||||||
|
remove(log_path);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv4[] = {"fastsync", "--chmod=u=rw,go=r", "--out-format=%f %l", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_STR(cfg->chmod_spec, "u=rw,go=r");
|
||||||
|
EXPECT_EQ_STR(cfg->out_format, "%f %l");
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv5[] = {"fastsync", "--chunk-size=4096", "--delta-max=1048576", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv5, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->chunk_size == 4096ULL);
|
||||||
|
EXPECT_TRUE(cfg->delta_max_file_size == 1048576ULL);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OPT_NOOP compatibility flags (-s/--secluded-args, -r/--recursive) must never
|
||||||
|
* swallow the next argv: `fastsync -s SRC DST` keeps both positionals. */
|
||||||
|
static void test_parse_args_noop_does_not_consume_argv() {
|
||||||
|
static const char* const noops[] = {"-s", "--secluded-args", "-r", "--recursive"};
|
||||||
|
for (size_t i = 0; i < sizeof(noops) / sizeof(noops[0]); i++) {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
char* argv[] = {"fastsync", (char*)noops[i], "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
char* argv[] = {"fastsync", "-sv", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -b/--backup and -L/--copy-links short aliases behave like their long forms. */
|
||||||
|
static void test_parse_args_backup_copy_links_shorts() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
char* argv_b[] = {"fastsync", "-b", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_b, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->backup);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
positional_count = 0;
|
||||||
|
char* argv_l[] = {"fastsync", "-L", "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_l, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->copy_links);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An unknown short option (alone or inside a cluster) is rejected, never
|
||||||
|
* silently ignored. */
|
||||||
|
static void test_parse_args_rejects_unsupported_short() {
|
||||||
|
static const char* const bad[] = {"-Q", "-aQ", "-rZ", "-9"};
|
||||||
|
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
char* argv[] = {"fastsync", (char*)bad[i], "/src", "/dst"};
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void test_client_cli() {
|
void test_client_cli() {
|
||||||
test_validate_config_required_paths();
|
test_validate_config_required_paths();
|
||||||
test_parse_args_numeric_ids();
|
test_parse_args_numeric_ids();
|
||||||
@@ -3801,4 +4000,10 @@ void test_client_cli() {
|
|||||||
test_parse_args_pattern_file_oversized_rejected();
|
test_parse_args_pattern_file_oversized_rejected();
|
||||||
test_parse_args_unsigned_options_reject_sign();
|
test_parse_args_unsigned_options_reject_sign();
|
||||||
test_validate_config_dry_run_rejects_write_batch();
|
test_validate_config_dry_run_rejects_write_batch();
|
||||||
|
test_parse_args_short_clustering();
|
||||||
|
test_parse_args_attached_short_values();
|
||||||
|
test_parse_args_inline_equals_forms();
|
||||||
|
test_parse_args_noop_does_not_consume_argv();
|
||||||
|
test_parse_args_backup_copy_links_shorts();
|
||||||
|
test_parse_args_rejects_unsupported_short();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,6 +234,31 @@ static void test_server_cli_password_requires_daemon() {
|
|||||||
server_cli_options_free(&opts);
|
server_cli_options_free(&opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --port is the rsync-style alias for -p, in both the separate and =value
|
||||||
|
* spellings; an invalid value is still validated. */
|
||||||
|
static void test_server_cli_port_alias() {
|
||||||
|
const char* a1[] = {"fastsync-server", "--port", "9000"};
|
||||||
|
ServerCliOptions opts;
|
||||||
|
EXPECT_EQ_INT(parse_ok(a1, 3, &opts), 0);
|
||||||
|
EXPECT_EQ_INT(opts.port, 9000);
|
||||||
|
EXPECT_TRUE(opts.port_set);
|
||||||
|
server_cli_options_free(&opts);
|
||||||
|
|
||||||
|
const char* a2[] = {"fastsync-server", "--port=9001"};
|
||||||
|
ServerCliOptions opts2;
|
||||||
|
EXPECT_EQ_INT(parse_ok(a2, 2, &opts2), 0);
|
||||||
|
EXPECT_EQ_INT(opts2.port, 9001);
|
||||||
|
EXPECT_TRUE(opts2.port_set);
|
||||||
|
server_cli_options_free(&opts2);
|
||||||
|
|
||||||
|
char err[128];
|
||||||
|
ServerCliOptions opts3;
|
||||||
|
const char* a3[] = {"fastsync-server", "--port", "notaport"};
|
||||||
|
EXPECT_EQ_INT(server_cli_parse(3, (char**)a3, &opts3, err, sizeof(err)), -1);
|
||||||
|
EXPECT_TRUE(strstr(err, "invalid port") != NULL);
|
||||||
|
server_cli_options_free(&opts3);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_server_cli_help() {
|
static void test_server_cli_help() {
|
||||||
char err[256];
|
char err[256];
|
||||||
const char* a1[] = {"s", "--help"};
|
const char* a1[] = {"s", "--help"};
|
||||||
@@ -254,5 +279,6 @@ void test_server_cli() {
|
|||||||
test_server_cli_password_requires_daemon();
|
test_server_cli_password_requires_daemon();
|
||||||
test_server_cli_no_super();
|
test_server_cli_no_super();
|
||||||
test_server_cli_allow_super();
|
test_server_cli_allow_super();
|
||||||
|
test_server_cli_port_alias();
|
||||||
test_server_cli_help();
|
test_server_cli_help();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user