rsync 3.4.1 drop-in parity (#285-#297) + parity completion (protocol 2.26.0) #298

Merged
TapTap merged 77 commits from feat/rsync-parity into dev 2026-09-17 20:33:17 +02:00
6 changed files with 545 additions and 103 deletions
Showing only changes of commit 23552e823d - Show all commits
+291 -87
View File
@@ -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,26 +1376,19 @@ 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;
}
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 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,29 +1571,24 @@ 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;
}
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; return true;
} }
if (opt_is(arg, "--chunk-size", NULL)) { if (opt_is(arg, "--chunk-size", NULL)) {
@@ -1506,23 +1597,13 @@ static bool cli_handle_io_options(CliParseCtx* ctx) {
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;
}
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; return true;
} }
if (opt_is(arg, "--log-file", NULL)) { if (opt_is(arg, "--log-file", NULL)) {
@@ -1531,33 +1612,8 @@ static bool cli_handle_io_options(CliParseCtx* ctx) {
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;
}
config->log_file = lf;
log_set_file(lf);
return true; return true;
} }
if (strncmp(arg, "--stderr=", 9) == 0) { if (strncmp(arg, "--stderr=", 9) == 0) {
@@ -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
View File
@@ -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
View File
@@ -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");
+13 -7
View File
@@ -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 (i + 1 >= argc) { if (inline_value) {
set_error(err, err_size, "missing argument for -p"); opts->port_set = true;
return -1; if (parse_port_arg(inline_value, &opts->port, err, err_size) != 0)
return -1;
} else {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for %s", argv[i]);
return -1;
}
opts->port_set = true;
if (parse_port_arg(argv[++i], &opts->port, err, err_size) != 0)
return -1;
} }
opts->port_set = true;
if (parse_port_arg(argv[++i], &opts->port, err, err_size) != 0)
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) {
+205
View File
@@ -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();
} }
+26
View File
@@ -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();
} }