Merge branch 'fix/parity-review-c' into feat/parity-fixes

This commit is contained in:
2026-09-18 22:11:07 +02:00
13 changed files with 153 additions and 63 deletions
+31 -7
View File
@@ -23,6 +23,7 @@
#include <langinfo.h>
#include <limits.h>
#include <locale.h>
#include <math.h>
#include <time.h>
#include <signal.h>
#include <stdbool.h>
@@ -501,6 +502,7 @@ static bool is_accepted_debug_category(const char* name) {
static bool is_accepted_info_category(const char* name) {
static const char* const categories[] = {
"backup",
"mount",
"syms",
"symsafe",
@@ -628,8 +630,6 @@ static int parse_info_flags(const char* value, Config* config) {
flag = LOG_INFO_FLIST;
else if (strcmp(name, "nonreg") == 0)
flag = LOG_INFO_NONREG;
else if (strcmp(name, "backup") == 0)
flag = LOG_INFO_BACKUP;
else if (strcmp(name, "progress") == 0)
flag = LOG_INFO_PROGRESS;
else if (is_accepted_info_category(name))
@@ -1897,17 +1897,41 @@ static int parse_bwlimit_value(const char* value, unsigned long long* bytes_per_
return -1;
}
long long size = 1;
long long base = 1;
for (int i = 0; i < reps; i++) {
if (size > LLONG_MAX / mult) {
if (base > LLONG_MAX / mult) {
log_message(LOG_LEVEL_ERROR, "--bwlimit=%s is too large", value);
return -1;
}
size *= mult;
base *= mult;
}
size = (long long)((double)size * atof(value));
/* rsync multiplies the numeric prefix (atof) by mult^reps in a signed
* ssize_t, which is undefined on overflow. Scale in double and range-check
* before converting, so a huge value is rejected as "too large" (where
* rsync's overflow happens to land on a negative result) without invoking
* signed-overflow UB. */
double scaled = (double)base * strtod(value, NULL);
/* (double)LLONG_MAX rounds up to 2^63, which is itself out of range for the
* cast, so reject at >= that bound; LLONG_MIN == -2^63 is exactly
* representable and thus castable, so the lower bound stays strict. */
if (!isfinite(scaled) || scaled >= (double)LLONG_MAX || scaled < (double)LLONG_MIN) {
log_message(LOG_LEVEL_ERROR, "--bwlimit=%s is too large", value);
return -1;
}
long long size = (long long)scaled;
if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != value) {
size += atoi(arg);
/* The only form accepted here is "+1"/"-1" (a longer number leaves a
trailing byte and is rejected below), so apply the delta directly and
guard the one overflow direction. */
if (*arg == '+') {
if (size == LLONG_MAX) {
log_message(LOG_LEVEL_ERROR, "--bwlimit=%s is too large", value);
return -1;
}
size += 1;
} else {
size -= 1;
}
arg += 2;
}
if (*arg != '\0' || size < 0) {
+5 -17
View File
@@ -229,7 +229,10 @@ static void transfer_stats_note_entry(TransferStats* stats, const File* file) {
}
/* Account for a regular file (or a whole-file append) the receiver actually
stored: rsync's transferred-file count and transferred/literal byte totals. */
stored: rsync's transferred-file count and transferred/literal byte totals.
`literal_data` counts the whole source size, which is exact for a whole-file
send but an upper bound for a delta send (the receiver reuses basis blocks
the sender never ships); see TransferStats.literal_data in format.h. */
static void transfer_stats_note_transferred(TransferStats* stats, const File* file) {
if (stats == NULL || file == NULL)
return;
@@ -940,8 +943,6 @@ static void source_file_destroy(void* item) {
}
}
static const char* delete_display_path(const Config* config, const char* path);
/* Remove only the same regular source file that was sent. */
static void remove_transferred_sources(const Config* config, ArrayList* paths) {
if (!config->remove_source_files || !paths)
@@ -1075,20 +1076,7 @@ static bool receive_stats_record(int fd, ReceiverStats* stats, ArrayList* would_
static const char* delete_display_path(const Config* config, const char* path) {
if (!config || !path || !config->send_directory)
return path;
const char* root = config->send_directory;
while (*root == '/')
root++;
const char* rel = path;
while (*rel == '/')
rel++;
size_t root_len = strlen(root);
while (root_len > 0 && root[root_len - 1] == '/')
root_len--;
if (root_len == 0)
return rel;
if (strncmp(rel, root, root_len) == 0 && (rel[root_len] == '/' || rel[root_len] == '\0'))
return rel + root_len + (rel[root_len] == '/' ? 1 : 0);
return rel;
return utils_strip_transfer_root(path, config->send_directory);
}
/* Send the final STATUS_FINISHED frame and await the receiver's verdict.
+2 -13
View File
@@ -438,18 +438,7 @@ static void scanner_record_protected(DirectoryScanner* scanner, const char* fs_p
static void scanner_note_nonreg(const ScannerOptions* options, const char* fs_path) {
if (!options || !options->note_nonreg || !fs_path)
return;
const char* rel = *fs_path == '/' ? fs_path + 1 : fs_path;
const char* root = options->send_directory;
if (root != NULL) {
while (*root == '/')
root++;
size_t root_len = strlen(root);
while (root_len > 0 && root[root_len - 1] == '/')
root_len--;
if (root_len > 0 && strncmp(root, rel, root_len) == 0 &&
(rel[root_len] == '/' || rel[root_len] == '\0'))
rel += root_len + (rel[root_len] == '/' ? 1 : 0);
}
const char* rel = utils_strip_transfer_root(fs_path, options->send_directory);
char* escaped = output_escape(rel, options->eight_bit_output);
printf("skipping non-regular file \"%s\"\n", escaped ? escaped : rel);
free(escaped);
@@ -947,7 +936,7 @@ static bool scanner_emit_empty_dir(DirectoryScanner* scanner, ArrayList* chunk_d
if (!scanner->current_path || !scanner->current_rel || scanner->current_rel[0] == '\0')
return true;
struct stat st;
if (stat(scanner->current_path, &st) != 0 || !S_ISDIR(st.st_mode))
if (lstat(scanner->current_path, &st) != 0 || !S_ISDIR(st.st_mode))
return true;
File* dir = scanner_build_dir_file(scanner->current_path, &st, &scanner->options);
if (!dir)
+1 -1
View File
@@ -82,7 +82,7 @@ typedef struct {
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
/* ===========================================================================
* Config wire-field table (single source of truth for protocol 2.26.0).
* Config wire-field table (single source of truth for protocol 2.27.0).
*
* Every field below crosses the wire. The table is the ONLY place a
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
+11 -12
View File
@@ -807,23 +807,22 @@ bool file_ensure_directory_secure(const char* path) {
} else if (errno == EEXIST) {
dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
}
} else if (dir_fd < 0 && (errno == ENOTDIR || errno == ELOOP)) {
/* rsync replaces a destination non-directory (regular file or symlink)
with an incoming directory. Confined to the already-opened secure
parent fd: the leaf is unlinked by name (never followed) and only a
non-directory is ever removed, so this cannot escape the authorized
root or remove a pre-existing directory tree. A symlink is left alone:
replacing it is not required for FastSync's transferred directories and
keeps --keep-dirlinks semantics untouched. */
} else if (dir_fd < 0 && errno == ENOTDIR) {
/* rsync replaces a destination non-directory (regular file) with an
incoming directory. Confined to the already-opened secure parent fd:
the leaf is unlinked by name (never followed) and only a non-directory
is ever removed, so this cannot escape the authorized root or remove a
pre-existing directory tree. A symlink is left alone (openat with
O_NOFOLLOW reports ELOOP, which takes no branch here), since replacing
it is not required for FastSync's transferred directories and keeps
--keep-dirlinks semantics untouched. */
struct stat leaf_st;
if (fstatat(parent_fd, leaf, &leaf_st, AT_SYMLINK_NOFOLLOW) == 0 && !S_ISDIR(leaf_st.st_mode) &&
!S_ISLNK(leaf_st.st_mode)) {
if (unlinkat(parent_fd, leaf, 0) == 0) {
if (mkdirat(parent_fd, leaf, (mode_t)(0777 & ~(mode_t)file_process_umask())) == 0) {
if (mkdirat(parent_fd, leaf, (mode_t)(0777 & ~(mode_t)file_process_umask())) == 0)
created = true;
} else if (errno != EEXIST) {
/* leave dir_fd < 0 so the caller sees the failure */
}
/* On failure dir_fd stays < 0 below, so the caller still sees it. */
dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
}
}
+6 -1
View File
@@ -88,7 +88,12 @@ typedef struct {
unsigned long long total_file_size; /* sum of entry sizes (link target len) */
unsigned long long transferred_regular; /* regular files actually stored */
unsigned long long transferred_file_size; /* source size of those files */
unsigned long long literal_data; /* literal bytes sent for them */
/* Whole-file accuracy: the `--stats` "Literal data" row. The sender counts
* the source size of every stored file, so a whole-file transfer matches
* rsync. A delta run actually ships only the literal fragments of the diff
* (the rest is matched/copied), so here the value is an upper bound, not
* rsync's literal-byte total; see RSYNC_COMPAT.md's `--stats` row. */
unsigned long long literal_data;
} TransferStats;
#endif
+5 -4
View File
@@ -24,17 +24,18 @@ typedef enum {
/* rsync categories that map to a FastSync event (emitted in rsync's line
* format): del (deletions), remove (sender-side source removal), name
* (transferred entry names), flist (file-list header), nonreg (skipped
* non-regular files), backup (backed-up files), progress (per-file progress). */
* non-regular files), progress (per-file progress). rsync's `backup`
* category is accepted for CLI parity but stays silent: the receiver does the
* backing-up and FastSync has no backup event to report from the sender. */
LOG_INFO_DEL = 1u << 4,
LOG_INFO_REMOVE = 1u << 5,
LOG_INFO_NAME = 1u << 6,
LOG_INFO_FLIST = 1u << 7,
LOG_INFO_NONREG = 1u << 8,
LOG_INFO_BACKUP = 1u << 9,
LOG_INFO_PROGRESS = 1u << 10,
LOG_INFO_PROGRESS = 1u << 9,
LOG_INFO_ALL = LOG_INFO_COPY | LOG_INFO_MISC | LOG_INFO_SKIP | LOG_INFO_STATS | LOG_INFO_DEL |
LOG_INFO_REMOVE | LOG_INFO_NAME | LOG_INFO_FLIST | LOG_INFO_NONREG |
LOG_INFO_BACKUP | LOG_INFO_PROGRESS,
LOG_INFO_PROGRESS,
} LogInfoFlag;
void log_message(LogLevel log_level, const char* message, ...);
+25
View File
@@ -52,6 +52,31 @@ bool path_is_within_root(const char* root, const char* path) {
return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/');
}
/* Borrowed transfer-relative view of `path`: strip any leading '/' and then a
* `root` prefix (its own leading/trailing slashes tolerated), returning a
* pointer into `path`. Non-allocating, so it is safe on the hot scan/print
* paths. A NULL/empty root, or a path not under `root`, leaves only the
* leading-slash strip. `path` must be NUL-terminated and live in the caller. */
const char* utils_strip_transfer_root(const char* path, const char* root) {
if (path == NULL)
return NULL;
const char* rel = path;
while (*rel == '/')
rel++;
if (root == NULL)
return rel;
while (*root == '/')
root++;
size_t root_len = strlen(root);
while (root_len > 0 && root[root_len - 1] == '/')
root_len--;
if (root_len == 0)
return rel;
if (strncmp(rel, root, root_len) == 0 && (rel[root_len] == '/' || rel[root_len] == '\0'))
return rel + root_len + (rel[root_len] == '/' ? 1 : 0);
return rel;
}
/* Open the destination root directory itself, confined to the authorized root.
* NOTE (do not merge with file_open_secure_parent): this walk opens dest_root
* (a directory that must already exist) and returns its fd, whereas
+5
View File
@@ -191,6 +191,11 @@ const char* utils_get_authorized_root_path(void);
* callers guarantee this); this is containment by string, not by resolved
* symlinks. Shared by the utils and file secure-walk root confinement. */
bool path_is_within_root(const char* root, const char* path);
/* Non-allocating transfer-relative view of `path`: strip any leading '/' and
* then a `root` prefix (leading/trailing slashes tolerated), returning a
* borrowed pointer into `path`. A NULL/empty root, or a path not under
* `root`, yields just the leading-slash strip. `path`/`root` must stay alive. */
const char* utils_strip_transfer_root(const char* path, const char* root);
/* True when `path` contains a ".." component. This is a purely lexical
* dot-dot check: an absolute path is NOT rejected here, because default
* (non-relative) transfers legitimately put the sender's absolute source path