CI / lint (pull_request) Successful in 26s
CI / sanitizers (address) (pull_request) Failing after 41s
CI / sanitizers (undefined) (pull_request) Successful in 40s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Failing after 37s
CI / build-and-test (pull_request) Failing after 2m35s
RSYNC_COMPAT Phase-2 row M: path-list construction and destination directory creation while preserving traversal safety. - -R/--relative with --files-from: transmit each listed entry under its bare relative destination path (no source-root mirror). Files keep an absolute local read path plus a separate wire/dest path (File.send_path); manifest, incremental quick-check and change output follow the wire path, so --delete and --remove-source-files stay consistent. -R without --files-from is unchanged (full mirror). - --no-implied-dirs: client-only, only meaningful with -R + --files-from. A listed file whose parent dir is not itself (or via an ancestor) explicitly listed cannot be placed; the run fails up front with a clear error. No effect otherwise. - --dirs/-d + --old-dirs/--old-d aliases: -d <dir> transmits the source root as an explicit empty directory entry (STATUS_MKDIR frame); with --files-from listed dirs are created empty and listed files transferred, never descending. Works single-threaded, -m (sequential scanner in the -m scan thread) and chunk-serialization (per-file type marker). Directory entries appear in the delete manifest. - --mkpath: new wire bool; server creates the destination root (and missing leading components under its authorized root) at connection start. A missing destination root is now rejected by default. - Protocol bumped to 2.7.0 (mkpath wire field + STATUS_MKDIR + chunk type marker). All receive paths funnel through file_save_to_disk_full which creates directories via the secure confined mkdir engine; dir entries are excluded from --remove-source-files outcome acknowledgements on both ends. - Unit coverage: CLI parse (relative/dirs aliases/mkpath/no-implied-dirs), config round-trip (relative + mkpath), scanner --dirs non-recursion and -R send_path (sequential + parallel), receiver dir-entry save. - Integration coverage: TestRelativeFilesFrom, TestNoImpliedDirs, TestDirs, TestMkpath (single and -m). - RSYNC_COMPAT: 4 rows move to Implemented (Summary 67/3/5/1/71 = 147).
330 lines
10 KiB
C
330 lines
10 KiB
C
#include "change_list.h"
|
|
#include "utils.h"
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
|
|
/* Itemize code emitted for a transferred regular file.
|
|
*
|
|
* Layout (rsync-compatible 11-char item): `>f` marks a regular file that was
|
|
* transferred to the remote host; the trailing nine markers are, in order,
|
|
* c(hecksum) s(ize) t(ime) p(erms) o(wner) g(roup) u(ser/acl) a(ttrs) x(attrs).
|
|
* Every marker is `+` (FastSync does not compare each attribute on the
|
|
* receiving side, so a sent file is reported as fully updated). Files that
|
|
* are already up to date print no line at all, matching rsync's single -i
|
|
* which only itemizes changes.
|
|
*
|
|
* Because the scanner only yields regular-file transfer candidates, `>d`
|
|
* (directory) lines are never produced; directories are not transferred as
|
|
* items by FastSync. */
|
|
#define ITEMIZE_SENT_FILE ">f+++++++++"
|
|
|
|
typedef struct {
|
|
char* data;
|
|
size_t length;
|
|
size_t capacity;
|
|
} StrBuf;
|
|
|
|
static void strbuf_free(StrBuf* buf) {
|
|
if (buf == NULL)
|
|
return;
|
|
free(buf->data);
|
|
buf->data = NULL;
|
|
buf->length = 0;
|
|
buf->capacity = 0;
|
|
}
|
|
|
|
static bool strbuf_reserve(StrBuf* buf, size_t extra) {
|
|
if (buf->length > SIZE_MAX - extra - 1)
|
|
return false;
|
|
size_t need = buf->length + extra + 1;
|
|
if (need <= buf->capacity)
|
|
return true;
|
|
size_t capacity = buf->capacity > 0 ? buf->capacity : 32;
|
|
while (capacity < need) {
|
|
if (capacity > SIZE_MAX / 2) {
|
|
capacity = need;
|
|
break;
|
|
}
|
|
capacity *= 2;
|
|
}
|
|
char* grown = realloc(buf->data, capacity);
|
|
if (!grown)
|
|
return false;
|
|
buf->data = grown;
|
|
buf->capacity = capacity;
|
|
return true;
|
|
}
|
|
|
|
static bool strbuf_append_char(StrBuf* buf, char c) {
|
|
if (!strbuf_reserve(buf, 1))
|
|
return false;
|
|
buf->data[buf->length++] = c;
|
|
buf->data[buf->length] = '\0';
|
|
return true;
|
|
}
|
|
|
|
static bool strbuf_append(StrBuf* buf, const char* text) {
|
|
if (text == NULL)
|
|
return true;
|
|
size_t length = strlen(text);
|
|
if (!strbuf_reserve(buf, length))
|
|
return false;
|
|
memcpy(buf->data + buf->length, text, length);
|
|
buf->length += length;
|
|
buf->data[buf->length] = '\0';
|
|
return true;
|
|
}
|
|
|
|
static bool strbuf_append_ull(StrBuf* buf, unsigned long long value) {
|
|
char digits[32];
|
|
int written = snprintf(digits, sizeof(digits), "%llu", value);
|
|
if (written < 0 || (size_t)written >= sizeof(digits))
|
|
return false;
|
|
return strbuf_append(buf, digits);
|
|
}
|
|
|
|
static bool strbuf_append_longlong(StrBuf* buf, long long value) {
|
|
char digits[32];
|
|
int written = snprintf(digits, sizeof(digits), "%lld", value);
|
|
if (written < 0 || (size_t)written >= sizeof(digits))
|
|
return false;
|
|
return strbuf_append(buf, digits);
|
|
}
|
|
|
|
bool change_list_enabled(const Config* config) {
|
|
return config != NULL && (config->itemize_changes || config->out_format != NULL ||
|
|
(config->log_file != NULL && config->log_file_format != NULL));
|
|
}
|
|
|
|
char* change_render_itemize(const ChangeEvent* event) {
|
|
if (event == NULL || event->decision != CHANGE_SENT)
|
|
return str_dup("");
|
|
const char* code = event->is_directory ? ">d+++++++++" : ITEMIZE_SENT_FILE;
|
|
StrBuf line = {0};
|
|
bool ok = strbuf_append(&line, code) && strbuf_append(&line, " ") &&
|
|
strbuf_append(&line, event->path != NULL ? event->path : "");
|
|
if (!ok) {
|
|
strbuf_free(&line);
|
|
return NULL;
|
|
}
|
|
return line.data;
|
|
}
|
|
|
|
static const char* leaf_name(const char* path) {
|
|
if (path == NULL)
|
|
return "";
|
|
const char* slash = strrchr(path, '/');
|
|
return slash != NULL && slash[1] != '\0' ? slash + 1 : path;
|
|
}
|
|
|
|
char* change_render_format(const char* format, const ChangeEvent* event) {
|
|
if (format == NULL)
|
|
return NULL;
|
|
StrBuf line = {0};
|
|
bool ok = true;
|
|
for (const char* p = format; *p != '\0' && ok;) {
|
|
if (*p != '%') {
|
|
ok = strbuf_append_char(&line, *p);
|
|
p++;
|
|
continue;
|
|
}
|
|
char token = p[1];
|
|
if (token == '\0') {
|
|
ok = strbuf_append_char(&line, '%');
|
|
break;
|
|
}
|
|
switch (token) {
|
|
case '%':
|
|
ok = strbuf_append_char(&line, '%');
|
|
break;
|
|
case 'f':
|
|
ok = strbuf_append(&line, event->path != NULL ? event->path : "");
|
|
break;
|
|
case 'n':
|
|
ok = strbuf_append(&line, leaf_name(event->path));
|
|
break;
|
|
case 'l':
|
|
ok = strbuf_append_ull(&line, event->size);
|
|
break;
|
|
case 'b':
|
|
ok = strbuf_append_ull(&line, event->bytes_sent);
|
|
break;
|
|
case 'M':
|
|
ok = strbuf_append_longlong(&line, (long long)event->mtime_sec);
|
|
break;
|
|
default:
|
|
/* Unknown escape sequences are preserved verbatim. */
|
|
ok = strbuf_append_char(&line, '%') && strbuf_append_char(&line, token);
|
|
break;
|
|
}
|
|
p += 2;
|
|
}
|
|
if (!ok) {
|
|
strbuf_free(&line);
|
|
return NULL;
|
|
}
|
|
if (line.data == NULL) {
|
|
line.data = str_dup("");
|
|
if (!line.data)
|
|
return NULL;
|
|
}
|
|
return line.data;
|
|
}
|
|
|
|
/* Format a mode as an `ls -l` permission string, e.g. `-rw-r--r--`. */
|
|
static void mode_to_ls_string(mode_t mode, char out[11]) {
|
|
out[0] = S_ISDIR(mode) ? 'd'
|
|
: S_ISLNK(mode) ? 'l'
|
|
: S_ISCHR(mode) ? 'c'
|
|
: S_ISBLK(mode) ? 'b'
|
|
: S_ISFIFO(mode) ? 'p'
|
|
: S_ISSOCK(mode) ? 's'
|
|
: '-';
|
|
mode_t bits = mode & 07777;
|
|
out[1] = (bits & S_IRUSR) ? 'r' : '-';
|
|
out[2] = (bits & S_IWUSR) ? 'w' : '-';
|
|
out[3] = (bits & S_IXUSR) ? (bits & S_ISUID ? 's' : 'x') : (bits & S_ISUID ? 'S' : '-');
|
|
out[4] = (bits & S_IRGRP) ? 'r' : '-';
|
|
out[5] = (bits & S_IWGRP) ? 'w' : '-';
|
|
out[6] = (bits & S_IXGRP) ? (bits & S_ISGID ? 's' : 'x') : (bits & S_ISGID ? 'S' : '-');
|
|
out[7] = (bits & S_IROTH) ? 'r' : '-';
|
|
out[8] = (bits & S_IWOTH) ? 'w' : '-';
|
|
out[9] = (bits & S_IXOTH) ? (bits & S_ISVTX ? 't' : 'x') : (bits & S_ISVTX ? 'T' : '-');
|
|
out[10] = '\0';
|
|
}
|
|
|
|
char* change_render_list_line(mode_t mode, unsigned long long size, time_t mtime,
|
|
const char* path) {
|
|
char permission[11];
|
|
mode_to_ls_string(mode, permission);
|
|
char date[32];
|
|
struct tm broken_down;
|
|
if (localtime_r(&mtime, &broken_down) != NULL) {
|
|
if (strftime(date, sizeof(date), "%Y/%m/%d %H:%M:%S", &broken_down) == 0)
|
|
snprintf(date, sizeof(date), "?");
|
|
} else {
|
|
snprintf(date, sizeof(date), "?");
|
|
}
|
|
StrBuf line = {0};
|
|
char size_field[32];
|
|
int written = snprintf(size_field, sizeof(size_field), "%llu", size);
|
|
if (written < 0 || (size_t)written >= sizeof(size_field)) {
|
|
strbuf_free(&line);
|
|
return NULL;
|
|
}
|
|
bool ok = strbuf_append(&line, permission) && strbuf_append_char(&line, ' ') &&
|
|
strbuf_append(&line, size_field) && strbuf_append_char(&line, ' ') &&
|
|
strbuf_append(&line, date) && strbuf_append_char(&line, ' ') &&
|
|
strbuf_append(&line, path != NULL ? path : "");
|
|
if (!ok) {
|
|
strbuf_free(&line);
|
|
return NULL;
|
|
}
|
|
return line.data;
|
|
}
|
|
|
|
static void print_escaped_line(FILE* stream, const char* line, bool eight_bit_output) {
|
|
char* escaped = output_escape(line, eight_bit_output);
|
|
if (escaped != NULL) {
|
|
fprintf(stream, "%s\n", escaped);
|
|
free(escaped);
|
|
} else {
|
|
fprintf(stream, "%s\n", line);
|
|
}
|
|
fflush(stream);
|
|
}
|
|
|
|
void change_emit(const Config* config, const ChangeEvent* event) {
|
|
if (event == NULL || !change_list_enabled(config))
|
|
return;
|
|
if (event->decision == CHANGE_UP_TO_DATE)
|
|
return;
|
|
bool to_stdout = config->itemize_changes || config->out_format != NULL;
|
|
bool to_log = config->log_file != NULL && config->log_file_format != NULL;
|
|
if (to_stdout) {
|
|
char* line = config->out_format != NULL ? change_render_format(config->out_format, event)
|
|
: change_render_itemize(event);
|
|
if (line != NULL) {
|
|
print_escaped_line(stdout, line, config->eight_bit_output);
|
|
free(line);
|
|
}
|
|
}
|
|
if (to_log) {
|
|
char* line = change_render_format(config->log_file_format, event);
|
|
if (line != NULL) {
|
|
print_escaped_line(config->log_file, line, config->eight_bit_output);
|
|
free(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool format_uses_mtime(const char* format) {
|
|
if (format == NULL)
|
|
return false;
|
|
/* Mirror change_render_format's tokenizer: "%%" is a literal percent (so
|
|
* "%%M" does NOT expand %M) and unknown "%X" escapes consume both chars.
|
|
* This keeps the optional stat() fallback below in step with the renderer. */
|
|
for (const char* p = format; *p != '\0';) {
|
|
if (*p != '%') {
|
|
p++;
|
|
continue;
|
|
}
|
|
char token = p[1];
|
|
if (token == '\0')
|
|
break;
|
|
if (token == 'M')
|
|
return true;
|
|
p += 2;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void change_emit_file_sent(const Config* config, const File* file) {
|
|
if (file == NULL || !change_list_enabled(config))
|
|
return;
|
|
ChangeEvent event;
|
|
memset(&event, 0, sizeof(event));
|
|
/* The displayed path is the one transmitted (with -R + --files-from this is
|
|
the bare relative destination path); the metadata fallback below still
|
|
stats the local absolute path. */
|
|
event.path = file_wire_path(file);
|
|
event.decision = CHANGE_SENT;
|
|
event.is_directory = false;
|
|
event.size = file->data != NULL ? file->data->size : 0;
|
|
/* FastSync has no wire-byte counter yet, so %b reports the source length
|
|
* that had to be delivered (always equal to %l); the actual bytes written
|
|
* to the socket (compressed/delta) are not measured. */
|
|
event.bytes_sent = event.size;
|
|
if (file->metadata != NULL) {
|
|
event.mtime_sec = file->metadata->mtime_sec;
|
|
} else if (format_uses_mtime(config->out_format) || format_uses_mtime(config->log_file_format)) {
|
|
/* Best-effort fallback for %M when no metadata was captured (no -M): the
|
|
* path is stat()ed just to fill the field, and any failure leaves 0. */
|
|
struct stat st;
|
|
if (file->path != NULL && stat(file->path, &st) == 0)
|
|
event.mtime_sec = st.st_mtime;
|
|
}
|
|
change_emit(config, &event);
|
|
}
|
|
|
|
/* Build and emit a CHANGE_SENT event for an explicit directory entry (-d). */
|
|
void change_emit_dir_sent(const Config* config, const File* file) {
|
|
if (file == NULL || !change_list_enabled(config))
|
|
return;
|
|
ChangeEvent event;
|
|
memset(&event, 0, sizeof(event));
|
|
event.path = file_wire_path(file);
|
|
event.decision = CHANGE_SENT;
|
|
event.is_directory = true;
|
|
event.size = 0;
|
|
event.bytes_sent = 0;
|
|
if (file->metadata != NULL)
|
|
event.mtime_sec = file->metadata->mtime_sec;
|
|
change_emit(config, &event);
|
|
}
|