fix(parity): client quick wins for rsync 3.4.1 (copy-links exit 23, info/debug flags, empty files-from, -F ordering, delete edges)
This commit is contained in:
+97
-25
@@ -395,6 +395,39 @@ static void apply_output_buffering(const Config* config) {
|
||||
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count,
|
||||
Config* config, char sign, const char* optname);
|
||||
|
||||
/* Split one --debug/--info item into its category name and an optional rsync
|
||||
* verbosity level suffix (e.g. "io2", "all4", "none0"). The output `name` is
|
||||
* NUL-terminated and `level` is >= 0 (0 silences the item). Returns false for
|
||||
* an empty token or a token that is all digits. */
|
||||
static bool split_flag_level(const char* token, char* name, size_t name_size, int* level) {
|
||||
size_t len = strlen(token);
|
||||
if (len == 0 || name_size == 0)
|
||||
return false;
|
||||
size_t end = len;
|
||||
while (end > 0 && token[end - 1] >= '0' && token[end - 1] <= '9')
|
||||
end--;
|
||||
if (end == 0)
|
||||
return false; /* all digits: not a category name */
|
||||
size_t name_len = end < name_size - 1 ? end : name_size - 1;
|
||||
for (size_t i = 0; i < name_len; i++) {
|
||||
char c = token[i];
|
||||
name[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
||||
}
|
||||
name[name_len] = '\0';
|
||||
int lvl = 1;
|
||||
if (end < len) {
|
||||
lvl = 0;
|
||||
for (size_t i = end; i < len; i++) {
|
||||
int digit = token[i] - '0';
|
||||
if (lvl > (1000 - digit) / 10)
|
||||
return false;
|
||||
lvl = lvl * 10 + digit;
|
||||
}
|
||||
}
|
||||
*level = lvl;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int parse_debug_flags(const char* value, Config* config) {
|
||||
if (!value || value[0] == '\0' || value[0] == ',' || value[strlen(value) - 1] == ',' ||
|
||||
strstr(value, ",,")) {
|
||||
@@ -412,30 +445,40 @@ static int parse_debug_flags(const char* value, Config* config) {
|
||||
for (char* token = strtok_r(flags, ",", &saveptr); token != NULL;
|
||||
token = strtok_r(NULL, ",", &saveptr)) {
|
||||
uint32_t flag = 0;
|
||||
if (strcmp(token, "help") == 0) {
|
||||
char name[32];
|
||||
int level = 1;
|
||||
if (!split_flag_level(token, name, sizeof(name), &level)) {
|
||||
log_message(LOG_LEVEL_ERROR, "unsupported --debug flag: %s", token);
|
||||
free(flags);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(name, "help") == 0) {
|
||||
print_debug_usage();
|
||||
free(flags);
|
||||
return 1;
|
||||
} else if (strcmp(token, "all") == 0) {
|
||||
parsed = LOG_DEBUG_ALL;
|
||||
} else if (strcmp(name, "all") == 0) {
|
||||
parsed = level == 0 ? 0 : LOG_DEBUG_ALL;
|
||||
continue;
|
||||
} else if (strcmp(token, "none") == 0) {
|
||||
} else if (strcmp(name, "none") == 0) {
|
||||
parsed = 0;
|
||||
continue;
|
||||
} else if (strcmp(token, "io") == 0) {
|
||||
} else if (strcmp(name, "io") == 0) {
|
||||
flag = LOG_DEBUG_IO;
|
||||
} else if (strcmp(token, "proto") == 0) {
|
||||
} else if (strcmp(name, "proto") == 0) {
|
||||
flag = LOG_DEBUG_PROTO;
|
||||
} else if (strcmp(token, "pack") == 0) {
|
||||
} else if (strcmp(name, "pack") == 0) {
|
||||
flag = LOG_DEBUG_PACK;
|
||||
} else if (strcmp(token, "util") == 0) {
|
||||
} else if (strcmp(name, "util") == 0) {
|
||||
flag = LOG_DEBUG_UTIL;
|
||||
} else {
|
||||
log_message(LOG_LEVEL_ERROR, "unsupported --debug flag: %s", token);
|
||||
free(flags);
|
||||
return -1;
|
||||
}
|
||||
parsed |= flag;
|
||||
if (level == 0)
|
||||
parsed &= ~flag;
|
||||
else
|
||||
parsed |= flag;
|
||||
}
|
||||
free(flags);
|
||||
config->debug_level = (int)parsed;
|
||||
@@ -461,28 +504,43 @@ static int parse_info_flags(const char* value, Config* config) {
|
||||
for (char* token = strtok_r(flags, ",", &saveptr); token != NULL;
|
||||
token = strtok_r(NULL, ",", &saveptr)) {
|
||||
uint32_t flag = 0;
|
||||
if (strcmp(token, "all") == 0) {
|
||||
parsed = LOG_INFO_ALL;
|
||||
char name[32];
|
||||
int level = 1;
|
||||
if (!split_flag_level(token, name, sizeof(name), &level)) {
|
||||
log_message(LOG_LEVEL_ERROR, "unsupported --info flag: %s", token);
|
||||
free(flags);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(name, "all") == 0) {
|
||||
parsed = level == 0 ? 0 : LOG_INFO_ALL;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(token, "none") == 0) {
|
||||
if (strcmp(name, "none") == 0) {
|
||||
parsed = 0;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(token, "copy") == 0)
|
||||
if (strcmp(name, "help") == 0) {
|
||||
print_info_usage();
|
||||
free(flags);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(name, "copy") == 0 || strcmp(name, "name") == 0)
|
||||
flag = LOG_INFO_COPY;
|
||||
else if (strcmp(token, "misc") == 0)
|
||||
else if (strcmp(name, "misc") == 0)
|
||||
flag = LOG_INFO_MISC;
|
||||
else if (strcmp(token, "skip") == 0)
|
||||
else if (strcmp(name, "skip") == 0)
|
||||
flag = LOG_INFO_SKIP;
|
||||
else if (strcmp(token, "stats") == 0)
|
||||
else if (strcmp(name, "stats") == 0)
|
||||
flag = LOG_INFO_STATS;
|
||||
else {
|
||||
log_message(LOG_LEVEL_ERROR, "unsupported --info flag: %s", token);
|
||||
free(flags);
|
||||
return -1;
|
||||
}
|
||||
parsed |= flag;
|
||||
if (level == 0)
|
||||
parsed &= ~flag;
|
||||
else
|
||||
parsed |= flag;
|
||||
}
|
||||
free(flags);
|
||||
config->info_level = (int)parsed;
|
||||
@@ -1027,17 +1085,22 @@ typedef struct {
|
||||
} CliParseCtx;
|
||||
|
||||
/* Apply output controls before processing other options so their order is
|
||||
* irrelevant. Returns 0 on success, -1 on error. */
|
||||
* irrelevant. Returns 0 on success, a positive code for a help request
|
||||
* (parse_args returns it verbatim), or -1 on error. */
|
||||
static int cli_apply_output_controls(Config* config, int argc, char* argv[]) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
||||
set_log_level(LOG_LEVEL_DEBUG);
|
||||
} else if (strncmp(argv[i], "--info=", 7) == 0) {
|
||||
if (parse_info_flags(argv[i] + 7, config) != 0)
|
||||
return -1;
|
||||
int ret = parse_info_flags(argv[i] + 7, config);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
} else if (strcmp(argv[i], "--info") == 0) {
|
||||
if (i + 1 >= argc || parse_info_flags(argv[++i], config) != 0)
|
||||
if (i + 1 >= argc)
|
||||
return -1;
|
||||
int ret = parse_info_flags(argv[++i], config);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -1842,13 +1905,19 @@ static bool cli_handle_logging_options(CliParseCtx* ctx) {
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--info=", 7) == 0) {
|
||||
if (parse_info_flags(arg + 7, config) != 0)
|
||||
ctx->exit_code = -1;
|
||||
int info_ret = parse_info_flags(arg + 7, config);
|
||||
if (info_ret != 0)
|
||||
ctx->exit_code = info_ret;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--info", NULL)) {
|
||||
if (ctx->i + 1 >= ctx->argc || parse_info_flags(ctx->argv[++ctx->i], config) != 0)
|
||||
if (ctx->i + 1 >= ctx->argc) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
int info_ret = parse_info_flags(ctx->argv[++ctx->i], config);
|
||||
if (info_ret != 0)
|
||||
ctx->exit_code = info_ret;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--skip-compress=", 16) == 0) {
|
||||
@@ -2436,8 +2505,11 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
}
|
||||
|
||||
int result = -1;
|
||||
if (cli_apply_output_controls(config, exp_argc, exp_argv) != 0)
|
||||
int output_ret = cli_apply_output_controls(config, exp_argc, exp_argv);
|
||||
if (output_ret != 0) {
|
||||
result = output_ret;
|
||||
goto done;
|
||||
}
|
||||
|
||||
CliParseCtx ctx = {
|
||||
.config = config,
|
||||
|
||||
+23
-18
@@ -328,15 +328,15 @@ static char* files_from_missing_dest_path(const Config* config, const char* entr
|
||||
|
||||
/* --files-from semantics: every listed entry must resolve under the source
|
||||
* root, otherwise rsync reports a hard error instead of silently transferring
|
||||
* nothing. An empty list is also an error. An entry of "." (the whole tree)
|
||||
* and listed-but-empty directories are valid. With --ignore-missing-args
|
||||
* nothing. An entry of "." (the whole tree) and listed-but-empty directories
|
||||
* are valid. An empty list is valid too: rsync transfers nothing and exits 0.
|
||||
* With --ignore-missing-args
|
||||
* (implied by --delete-missing-args) a listed-but-missing entry is instead
|
||||
* skipped: nothing is transferred for it, it never enters the keep-set and the
|
||||
* run succeeds for the rest (an all-missing non-empty list succeeds
|
||||
* transferring nothing, matching rsync). With --delete-missing-args
|
||||
* `missing_dest` (when non-NULL) collects the entry's destination-relative
|
||||
* mirror for the receiver's exact-deletion request. An empty list stays a
|
||||
* hard error in every mode (nothing was requested at all). Runs before any
|
||||
* mirror for the receiver's exact-deletion request. Runs before any
|
||||
* transfer so the failure/skip is surfaced uniformly in the single-threaded,
|
||||
* -m, dry-run and --list-only paths. */
|
||||
static bool files_from_list_check(const Config* config, ArrayList* missing_dest, int* skipped_out) {
|
||||
@@ -349,12 +349,11 @@ static bool files_from_list_check(const Config* config, ArrayList* missing_dest,
|
||||
return false;
|
||||
}
|
||||
if (set->count == 0) {
|
||||
char* escaped_list =
|
||||
output_escape(config->files_from ? config->files_from : "", log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_ERROR, "--files-from file '%s' contains no entries; nothing to transfer",
|
||||
escaped_list ? escaped_list : "<allocation failed>");
|
||||
free(escaped_list);
|
||||
return false;
|
||||
/* rsync treats an empty --files-from list as "nothing to transfer" and
|
||||
exits 0 (the source directory is still a valid source arg), so this is
|
||||
not an error. Nothing passes the (empty) allow-set, so no file is sent
|
||||
and no keep-set entry is produced. */
|
||||
return true;
|
||||
}
|
||||
bool ignore = config->ignore_missing_args || config->delete_missing_args;
|
||||
for (int i = 0; i < set->count; i++) {
|
||||
@@ -2679,12 +2678,15 @@ int send_files(Config* config) {
|
||||
report_transfer_stats(config, total_files, total_bytes, start);
|
||||
log_info_message(LOG_INFO_STATS, "Transfer summary: %d files, %.1f MB", total_files,
|
||||
(double)total_bytes / (double)BYTES_PER_MIB);
|
||||
/* --ignore-errors: an unreadable source directory was skipped but the run
|
||||
still completed (and deleted); report the run as errored like rsync does.
|
||||
A --max-delete-capped commit is a successful transfer that rsync reports
|
||||
/* A skipped source entry (--ignore-errors past an unreadable directory, or a
|
||||
dereferenced symlink with no referent) makes rsync report a partial
|
||||
transfer (exit 23) even though the rest of the run succeeded. A
|
||||
--max-delete-capped commit is a successful transfer that rsync reports
|
||||
with exit code 25. */
|
||||
if (!ok || had_scan_io)
|
||||
if (!ok)
|
||||
ret = 1;
|
||||
else if (had_scan_io)
|
||||
ret = 23;
|
||||
else
|
||||
ret = delete_limit ? 25 : 0;
|
||||
|
||||
@@ -2921,12 +2923,15 @@ int send_files_multithreaded(Config** config_ptr) {
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
bool sender_ok = sender_result == thrd_success;
|
||||
bool delete_limit = context->delete_limit;
|
||||
/* --ignore-errors: the run completed (and deleted) past an unreadable source
|
||||
directory; report it as errored like rsync does. A --max-delete-capped
|
||||
commit is a successful transfer that rsync reports with exit code 25. */
|
||||
/* A skipped source entry (--ignore-errors past an unreadable directory, or a
|
||||
dereferenced symlink with no referent) makes rsync report a partial
|
||||
transfer (exit 23). A --max-delete-capped commit is a successful transfer
|
||||
that rsync reports with exit code 25. */
|
||||
pipeline_context_sender_destroy(context);
|
||||
client_set_abort_armed(false);
|
||||
if (!sender_ok || scan_io)
|
||||
if (!sender_ok)
|
||||
return 1;
|
||||
if (scan_io)
|
||||
return 23;
|
||||
return delete_limit ? 25 : 0;
|
||||
}
|
||||
|
||||
+16
-2
@@ -163,6 +163,12 @@ typedef struct {
|
||||
Size pruning protects the destination mirror even under --delete-excluded,
|
||||
so it is recorded into a separate sink from `excluded`. */
|
||||
bool size_excluded;
|
||||
/* True when a symlink selected for dereferencing (-L/--copy-links or an
|
||||
unsafe target under --copy-unsafe-links) had no usable referent (a broken
|
||||
link or a stat() failure). rsync still reports this as a partial transfer
|
||||
(exit 23) even though the entry is skipped, so the scanner records it as a
|
||||
non-fatal I/O error. */
|
||||
bool referent_error;
|
||||
} ScannerEntry;
|
||||
|
||||
/* --one-file-system (-x) decision. Only directories can carry a different
|
||||
@@ -412,6 +418,7 @@ static int scanner_inspect_entry(const ScannerOptions* options, const char* cont
|
||||
const char* link_rel, const char* name, ScannerEntry* entry) {
|
||||
entry->excluded = false;
|
||||
entry->size_excluded = false;
|
||||
entry->referent_error = false;
|
||||
entry->is_symlink = false;
|
||||
entry->link_target = NULL;
|
||||
entry->path = path_cat(containing_dir, name);
|
||||
@@ -438,12 +445,13 @@ static int scanner_inspect_entry(const ScannerOptions* options, const char* cont
|
||||
goto skip;
|
||||
case LINK_ACTION_DEREF:
|
||||
if (stat(entry->path, &entry->stats) != 0) {
|
||||
/* rsync reports "symlink has no referent" and continues (exit 23); we
|
||||
surface the same condition rather than silently dropping the entry. */
|
||||
/* rsync reports "symlink has no referent" and continues with a partial
|
||||
transfer (exit 23); record the error so the run exits 23 too. */
|
||||
char* escaped = output_escape(entry->path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "symlink has no referent: %s",
|
||||
escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
entry->referent_error = true;
|
||||
goto skip;
|
||||
}
|
||||
entry->is_directory = S_ISDIR(entry->stats.st_mode);
|
||||
@@ -1118,6 +1126,10 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
break;
|
||||
}
|
||||
if (inspection == 0) {
|
||||
/* A dereferenced symlink with no referent is a partial-transfer error
|
||||
(rsync exit 23): record it as a non-fatal scan I/O error. */
|
||||
if (inspected.referent_error)
|
||||
scanner->io_error = true;
|
||||
/* A user-selection exclude protects its destination mirror from --delete
|
||||
unless --delete-excluded; a size prune is always protected. Other
|
||||
skips (unreadable, symlink policy) protect nothing. Under -R +
|
||||
@@ -1511,6 +1523,8 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
|
||||
return;
|
||||
}
|
||||
if (inspection == 0) {
|
||||
if (inspected.referent_error)
|
||||
ps->io_error = true;
|
||||
ArrayList* sink = NULL;
|
||||
if (inspected.excluded)
|
||||
sink = inspected.size_excluded ? options->size_skipped_paths : options->excluded_paths;
|
||||
|
||||
+11
-3
@@ -171,8 +171,8 @@ void print_usage(void) {
|
||||
printf(" -v, --verbose Enable debug logging\n");
|
||||
printf(" -q, --quiet Suppress non-error output\n");
|
||||
printf(" --debug=FLAGS Fine-grained debug logging (use --debug=help for flags)\n");
|
||||
printf(" --info=FLAGS Fine-grained info: copy,misc,skip,stats,all,none\n");
|
||||
printf(" none suppresses info even with --verbose\n");
|
||||
printf(" --info=FLAGS Fine-grained info: copy,name,misc,skip,stats,all,none\n");
|
||||
printf(" (use --info=help for flags; none suppresses --verbose)\n");
|
||||
printf(" --preserve Preserve permissions and times (= -pt; long form only)\n");
|
||||
printf(" --no-perms Negate -p/--perms\n");
|
||||
printf(" --no-times Negate -t/--times\n");
|
||||
@@ -340,5 +340,13 @@ void print_usage(void) {
|
||||
void print_debug_usage(void) {
|
||||
printf("Supported debug flags: IO,PROTO,PACK,UTIL,ALL,NONE\n");
|
||||
printf("Flags may be comma-separated, for example: --debug=io,proto\n");
|
||||
printf("Other rsync debug flags are unsupported and rejected.\n");
|
||||
printf("An optional level suffix is accepted (e.g. --debug=io2); level 0\n");
|
||||
printf("silences that item. Other rsync debug flags are unsupported and rejected.\n");
|
||||
}
|
||||
|
||||
void print_info_usage(void) {
|
||||
printf("Supported info flags: COPY,NAME,MISC,SKIP,STATS,ALL,NONE\n");
|
||||
printf("Flags may be comma-separated, for example: --info=name,stats\n");
|
||||
printf("An optional level suffix is accepted (e.g. --info=stats2); level 0\n");
|
||||
printf("silences that item. Other rsync info flags are unsupported and rejected.\n");
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
void print_usage(void);
|
||||
void print_debug_usage(void);
|
||||
void print_info_usage(void);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user