#include "client_send_internal.h" #include "array_list.h" #include "change_list.h" #include "charset.h" #include "config.h" #include "file.h" #include "format.h" #include "log.h" #include "protocol.h" #include "utils.h" #include #include #include #include #include /* Surface a server rejection to the user. When the last status exchange carried a STATUS_ERROR_DETAIL reason (protocol 2.21.0) it is appended to the client-side context; a bare STATUS_ERROR still logs the context alone. */ void log_server_rejection(const char* context) { const char* detail = protocol_last_error(); if (detail && detail[0] != '\0') { /* The detail is peer-controlled: escape it so terminal/log-format * metacharacters cannot be injected into the client's output. */ char* escaped = output_escape(detail, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "%s: %s", context, escaped ? escaped : ""); free(escaped); } else { log_message(LOG_LEVEL_ERROR, "%s", context); } } const char* display_bytes(unsigned long long bytes, bool human_readable, char* buffer, size_t buffer_size) { if (human_readable && format_human_size_decimal(bytes, buffer, buffer_size)) return buffer; snprintf(buffer, buffer_size, "%.1f MB", (double)bytes / (double)BYTES_PER_MIB); return buffer; } /* rsync byte count: human-readable decimal when -h was given, otherwise a * comma-grouped integer (rsync's big_num in the C locale). */ static const char* stats_bytes(const Config* config, unsigned long long bytes, char* buffer, size_t buffer_size) { if (!format_big_num(bytes, config->human_readable, buffer, buffer_size)) snprintf(buffer, buffer_size, "%llu", bytes); return buffer; } /* Build rsync's per-type parenthetical: each non-zero category, in reg/dir/link/special order. Empty when every count is zero. */ static void type_breakdown(unsigned long long reg, unsigned long long dir, unsigned long long link, unsigned long long special, char* out, size_t out_size) { if (reg + dir + link + special == 0) { out[0] = '\0'; return; } out[0] = '\0'; size_t used = 0; const struct { const char* name; unsigned long long count; } parts[4] = {{"reg", reg}, {"dir", dir}, {"link", link}, {"special", special}}; bool first = true; for (size_t i = 0; i < 4; i++) { if (parts[i].count == 0) continue; int written = snprintf(out + used, out_size - used, "%s%s: %llu", first ? "(" : ", ", parts[i].name, parts[i].count); if (written < 0 || (size_t)written >= out_size - used) break; used += (size_t)written; first = false; } if (!first && used + 1 < out_size) out[used++] = ')'; out[used] = '\0'; } /* Build rsync's `Number of files` parenthetical from the scan's flist counts. */ static void stats_type_breakdown(const TransferStats* stats, char* out, size_t out_size) { type_breakdown(stats->flist_reg, stats->flist_dir, stats->flist_link, stats->flist_special, out, out_size); } /* rsync's `Number of files` counts every directory. A recursive scan that preserves a directory attribute captures them in `dir_entries`; a `-r` scan (no -t/-p) captures nothing, so fall back to the scanner's shared counter of traversed directories that are not already represented by an inline directory entry. The -d generator counts its explicit directory entries inline and does not traverse, so it is excluded here. */ unsigned long long dir_count_for_stats(const Config* config, const ArrayList* dir_entries, atomic_ullong* counter) { if (config == NULL || config->dirs || config->list_only) return 0; if (dir_metadata_should_capture(config)) return dir_entries != NULL ? (unsigned long long)dir_entries->size : 0; return counter != NULL ? (unsigned long long)atomic_load(counter) : 0; } /* Print the rsync `--stats` block on stdout. The source-side flist and transferred counters come from `stats` (filled while scanning/sending), the receiver-only counters from the STATUS_STATS frame, and the wire byte totals from the process-wide protocol counters. The labels, layout and rate/speedup formulas match rsync 3.4.1. Shared by the single-threaded and multithreaded send paths. */ void report_transfer_stats(const Config* config, const TransferStats* stats, time_t start, const ReceiverStats* recv) { if (!config->stats || config->quiet) return; TransferStats empty = {0}; if (stats == NULL) stats = ∅ ReceiverStats none = {0}; if (recv == NULL) recv = &none; unsigned long long sent = protocol_bytes_written(); unsigned long long received = protocol_bytes_read(); /* rsync: bytes_per_sec = (written + read) / (0.5 + (end - start)). */ double elapsed = difftime(time(NULL), start); double rate = (double)(sent + received) / (0.5 + elapsed); char total_buffer[32]; char transferred_buffer[32]; char literal_buffer[32]; char matched_buffer[32]; char sent_buffer[32]; char recv_buffer[32]; char rate_buffer[32] = {0}; char human_rate[32] = {0}; const char* total = stats_bytes(config, stats->total_file_size, total_buffer, sizeof(total_buffer)); const char* transferred = stats_bytes(config, stats->transferred_file_size, transferred_buffer, sizeof(transferred_buffer)); /* Protocol 2.28.0: the receiver reports the bytes it literally stored, which is exact for a delta transfer (the sender's own literal_data counts each stored file's whole source size and is only an upper bound). Fall back to the sender total when the receiver reported no delta/literal accounting (e.g. a local no-server path). */ unsigned long long literal_bytes = (recv->literal_bytes != 0 || recv->matched_data != 0) ? recv->literal_bytes : stats->literal_data; const char* literal = stats_bytes(config, literal_bytes, literal_buffer, sizeof(literal_buffer)); const char* sent_s = stats_bytes(config, sent, sent_buffer, sizeof(sent_buffer)); const char* recv_s = stats_bytes(config, received, recv_buffer, sizeof(recv_buffer)); const char* rate_str = rate_buffer; if (config->human_readable) { if (!format_human_size_decimal((unsigned long long)rate, human_rate, sizeof(human_rate))) snprintf(human_rate, sizeof(human_rate), "0"); rate_str = human_rate; } else { snprintf(rate_buffer, sizeof(rate_buffer), "%.2f", rate); } double speedup = (sent + received) > 0 ? (double)stats->total_file_size / (double)(sent + received) : 0.0; char breakdown[128]; stats_type_breakdown(stats, breakdown, sizeof(breakdown)); unsigned long long flist_total = stats->flist_reg + stats->flist_dir + stats->flist_link + stats->flist_special; char created_breakdown[128]; type_breakdown(recv->created_reg, recv->created_dir, recv->created_link, recv->created_special, created_breakdown, sizeof(created_breakdown)); unsigned long long created_total = recv->created_reg + recv->created_dir + recv->created_link + recv->created_special; printf("\n"); if (breakdown[0] != '\0') printf("Number of files: %llu %s\n", flist_total, breakdown); else printf("Number of files: %llu\n", flist_total); /* Protocol 2.28.0: the receiver reports which destination entries it newly created, split by type, so this line matches rsync exactly. */ if (created_breakdown[0] != '\0') printf("Number of created files: %llu %s\n", created_total, created_breakdown); else printf("Number of created files: %llu\n", created_total); printf("Number of deleted files: %llu\n", recv->deleted_files); printf("Number of regular files transferred: %llu\n", stats->transferred_regular); printf("Total file size: %s bytes\n", total); printf("Total transferred file size: %s bytes\n", transferred); printf("Literal data: %s bytes\n", literal); const char* matched = stats_bytes(config, recv->matched_data, matched_buffer, sizeof(matched_buffer)); printf("Matched data: %s bytes\n", matched); printf("File list size: 0\n"); printf("File list generation time: 0.000 seconds\n"); printf("File list transfer time: 0.000 seconds\n"); printf("Total bytes sent: %s\n", sent_s); printf("Total bytes received: %s\n", recv_s); printf("\n"); printf("sent %s bytes received %s bytes %s bytes/sec\n", sent_s, recv_s, rate_str); printf("total size is %s speedup is %.2f%s\n", total, speedup, config->dry_run ? " (DRY RUN)" : ""); fflush(stdout); } /* Classify one scanned source entry into the rsync flist counters. Called for every entry the sender walks, transferred or skipped. Directory entries are counted here only for the explicit -d/--dirs generator; a recursive scan's directories are accounted from the scanner's dir_entries list at report time. */ void transfer_stats_note_entry(TransferStats* stats, const File* file) { if (stats == NULL || file == NULL) return; if (file->is_dir) { stats->flist_dir++; return; } if (file->is_symlink) { stats->flist_link++; stats->total_file_size += file->symlink_target ? strlen(file->symlink_target) : 0; return; } if (file->is_special) { stats->flist_special++; return; } stats->flist_reg++; stats->total_file_size += file->data ? file->data->size : 0; } /* Account for a regular file (or a whole-file append) the receiver actually 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. */ void transfer_stats_note_transferred(TransferStats* stats, const File* file) { if (stats == NULL || file == NULL) return; if (file->is_dir || file->is_symlink || file->is_special) return; if (file->link_group != 0 && !file->link_first) return; unsigned long long size = file->data ? file->data->size : 0; stats->transferred_regular++; stats->transferred_file_size += size; stats->literal_data += size; } /* ---- rsync-style per-file --progress ------------------------------------ * rsync prints, for each transferred regular file, the file name followed by a * two-frame progress line: the first at the initial 32 KiB read window (always * 0.00 kB/s / 0:00:00 on a sub-second transfer) and a final 100% frame carrying * `(xfr#N, to-chk=X/Y)`. Rates are wall-clock dependent, so only the final * rate is measured here; the layout matches rsync 3.4.1's progress.c. */ #define RSYNC_PROGRESS_IO_WINDOW (32ULL * 1024ULL) /* Paths-only pre-count of the source file list, built once at transfer start * when progress output or -i/--out-format needs it. rsync's `to-chk` * denominator is the whole file list -- every regular file, directory, symlink * and special plus the transfer root -- while the streaming scan only emits * empty directories. A metadata-only walk (no file reads, no hashing) supplies * that total and a metadata-bearing File for every directory, so --progress can * name them and -i/--out-format can itemize them without a second full scan. */ /* One directory in the pre-count, keyed by its transfer-relative display name * ("" is the transfer root). `file` is owned by ProgressPrecount.dir_files and * carries the source metadata needed by -i/--out-format (%M/%B/%U/%G). */ typedef struct { char* name; /* owned */ File* file; } DirRef; typedef struct { unsigned long long total; ArrayList* dir_paths; /* owned char* in transfer-relative display form */ ArrayList* dir_files; /* owned File* captured during the metadata walk */ ArrayList* dir_refs; /* owned DirRef*, sorted by name for prefix lookup */ } ProgressPrecount; static bool g_progress_active; /* True when -i/--out-format need the pre-counted directory entries fed into the * change-event stream (independent of --progress). */ static bool g_change_dirs_active; static unsigned long long g_progress_xferred; static unsigned long long g_progress_index; static unsigned long long g_progress_total; static struct timespec g_progress_file_start; static ProgressPrecount g_progress_precount; static PathIndex g_progress_dir_index; static bool g_progress_dir_index_valid; static StrHashSet g_progress_emitted; static bool g_progress_emitted_valid; static ArrayList* g_progress_emitted_keys; bool progress_requested(const Config* config) { return config != NULL && !config->quiet && (config->show_progress || (config->info_level & LOG_INFO_PROGRESS) != 0); } static void dir_ref_destroy(void* item) { DirRef* ref = (DirRef*)item; if (ref == NULL) return; free(ref->name); free(ref); } /* Sort DirRef pointers by their transfer-relative name for binary search. */ static int dir_ref_compare(const void* left, const void* right) { const DirRef* a = *(const DirRef* const*)left; const DirRef* b = *(const DirRef* const*)right; return strcmp(a->name, b->name); } /* Look up the pre-counted directory File for a transfer-relative name ("" is * the transfer root). Returns NULL when no pre-count was built or the name is * not a known directory. */ static File* progress_dir_lookup(const char* name) { if (name == NULL || g_progress_precount.dir_refs == NULL) return NULL; ArrayList* refs = g_progress_precount.dir_refs; size_t lo = 0; size_t hi = (size_t)refs->size; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; DirRef* ref = (DirRef*)refs->items[mid]; int cmp = strcmp(ref->name, name); if (cmp < 0) lo = mid + 1; else if (cmp > 0) hi = mid; else return ref->file; } return NULL; } static void progress_precount_dispose(ProgressPrecount* p) { if (p->dir_paths != NULL) { array_list_delete(p->dir_paths); p->dir_paths = NULL; } if (p->dir_files != NULL) { array_list_delete(p->dir_files); p->dir_files = NULL; } if (p->dir_refs != NULL) { array_list_delete(p->dir_refs); p->dir_refs = NULL; } p->total = 0; } /* Record the transfer root's pre-transfer state for -i/--out-format. The * receive root always exists, so rsync never marks it `cd`; its only observable * change is its timestamp, which FastSync cannot observe remotely. Force a time * mismatch so the root renders rsync's `.d..t...... ./` rather than the `cd` * a zeroed destination state would produce. */ static void progress_precount_mark_root(File* root) { if (root == NULL) return; root->dest_state.known = true; root->dest_state.existed = true; root->dest_state.mode = root->metadata != NULL ? root->metadata->mode : 0; root->dest_state.uid = root->metadata != NULL ? root->metadata->uid : 0; root->dest_state.gid = root->metadata != NULL ? root->metadata->gid : 0; root->dest_state.size = 0; root->dest_state.mtime_sec = (root->metadata != NULL ? root->metadata->mtime_sec : 0) - 3600; root->dest_state.mtime_nsec = root->metadata != NULL ? root->metadata->mtime_nsec : 0; } /* Append one DirRef (name -> file) to the pre-count, marking the transfer * root's destination state. Returns false on allocation failure. */ static bool progress_precount_add_ref(ProgressPrecount* p, const Config* config, File* file) { const char* rel = delete_display_path(config, file_wire_path(file)); char* name = rel != NULL ? str_dup(rel) : NULL; if (name == NULL) return false; DirRef* ref = malloc(sizeof(*ref)); if (ref == NULL) { free(name); return false; } ref->name = name; ref->file = file; if (name[0] == '\0') progress_precount_mark_root(file); if (!array_list_add(p->dir_refs, ref)) { dir_ref_destroy(ref); return false; } return true; } void client_progress_cleanup(void) { if (g_progress_dir_index_valid) { path_index_free(&g_progress_dir_index); g_progress_dir_index_valid = false; } if (g_progress_emitted_valid) { str_hash_set_free(&g_progress_emitted); g_progress_emitted_valid = false; } if (g_progress_emitted_keys != NULL) { array_list_delete(g_progress_emitted_keys); g_progress_emitted_keys = NULL; } progress_precount_dispose(&g_progress_precount); g_progress_active = false; g_change_dirs_active = false; g_progress_total = 0; g_progress_index = 0; g_progress_xferred = 0; } static void progress_first_frame(unsigned long long size, char* out, size_t out_size) { char ofs_buf[32]; unsigned long long ofs = size < RSYNC_PROGRESS_IO_WINDOW ? size : RSYNC_PROGRESS_IO_WINDOW; if (!format_big_num(ofs, false, ofs_buf, sizeof(ofs_buf))) snprintf(ofs_buf, sizeof(ofs_buf), "%llu", ofs); int pct = size == 0 ? 100 : (ofs == size ? 100 : (int)(100.0 * (double)ofs / (double)size)); snprintf(out, out_size, "\r%15s %3d%% %7.2f%s %s%s", ofs_buf, pct, 0.0, "kB/s", " 0:00:00", " "); } static void progress_final_frame(unsigned long long size, char* out, size_t out_size) { char ofs_buf[32]; char rembuf[32]; unsigned long long last_ofs = size < RSYNC_PROGRESS_IO_WINDOW ? size : RSYNC_PROGRESS_IO_WINDOW; if (!format_big_num(size, false, ofs_buf, sizeof(ofs_buf))) snprintf(ofs_buf, sizeof(ofs_buf), "%llu", size); struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); long long diff_ms = (long long)(now.tv_sec - g_progress_file_start.tv_sec) * 1000 + (now.tv_nsec - g_progress_file_start.tv_nsec) / 1000000; if (diff_ms <= 0) diff_ms = 1; double rate = size > last_ofs ? (double)(size - last_ofs) * 1000.0 / (double)diff_ms / 1024.0 : 0.0; const char* units = "kB/s"; if (rate > 1024.0 * 1024.0) { rate /= 1024.0 * 1024.0; units = "GB/s"; } else if (rate > 1024.0) { rate /= 1024.0; units = "MB/s"; } unsigned long long remain = (unsigned long long)(diff_ms / 1000); snprintf(rembuf, sizeof(rembuf), "%4u:%02u:%02u", (unsigned)(remain / 3600), (unsigned)((remain / 60) % 60), (unsigned)(remain % 60)); /* rsync's `to-chk` denominator is the whole file list (the pre-count); the numerator falls as each entry is processed, root first. Without a pre-count (the paths-only walk failed) fall back to the transferred-file count so the single-file layout stays intact. */ unsigned long long total = g_progress_total > 0 ? g_progress_total : g_progress_xferred + 1; unsigned long long to_chk = total > g_progress_index ? total - g_progress_index - 1 : 0; snprintf(out, out_size, "\r%15s %3d%% %7.2f%s %s (xfr#%llu, to-chk=%llu/%llu)\n", ofs_buf, 100, rate, units, rembuf, g_progress_xferred, to_chk, total); } bool info_flag_enabled(const Config* config, LogInfoFlag flag) { return config != NULL && (config->info_level & flag) != 0; } /* Print rsync's deletion lines for a received list of destination-relative * paths: `*deleting PATH` when itemizing, the --out-format expansion when a * format is set, else `deleting PATH` for --info=del. Used by both the dry-run * would-delete report and the real --info=del report. */ void print_delete_reports(const Config* config, const ArrayList* paths) { if (!config || !paths || config->quiet) return; /* --debug=del is independent of the --info=del/itemize/out-format display: emit the debug trace even when no deletion line would be printed. */ if (log_debug_enabled(LOG_DEBUG_DEL)) { for (int i = 0; i < paths->size; i++) { const char* raw = (const char*)paths->items[i]; const char* path = delete_display_path(config, raw); log_debug_message(LOG_DEBUG_DEL, "del: %s", path ? path : raw); } } if (!(config->itemize_changes || config->out_format != NULL || info_flag_enabled(config, LOG_INFO_DEL))) return; for (int i = 0; i < paths->size; i++) { const char* raw = (const char*)paths->items[i]; const char* path = delete_display_path(config, raw); if (config->out_format != NULL) { ChangeEvent event; memset(&event, 0, sizeof(event)); event.decision = CHANGE_SENT; event.deleted = true; event.name = path; event.path = path; char* line = change_render_format(config->out_format, config, &event); if (line) { char* escaped = output_escape(line, config->eight_bit_output); printf("%s\n", escaped ? escaped : line); free(escaped); free(line); } } else { char* escaped = output_escape(path, config->eight_bit_output); if (config->itemize_changes) printf("*deleting %s\n", escaped ? escaped : path); else printf("deleting %s\n", escaped ? escaped : path); free(escaped); } } fflush(stdout); } /* Emit every not-yet-seen ancestor directory of `rel`, outermost first, in the * order rsync's depth-first flist walk visits them. With -i/--out-format each * ancestor becomes a real change line (`cd+++++++++ sub/`, `.d..t...... ./`) * rendered by the shared itemize code; otherwise it is the `--info=name` / * --progress directory name line. */ static void client_progress_emit_ancestors(const Config* config, const char* rel) { if (!g_progress_dir_index_valid || !g_progress_emitted_valid || g_progress_emitted_keys == NULL || rel == NULL) return; size_t rel_len = strlen(rel); for (size_t i = 0; i < rel_len; i++) { if (rel[i] != '/') continue; char* prefix = malloc(i + 1); if (prefix == NULL) return; memcpy(prefix, rel, i); prefix[i] = '\0'; if (path_index_contains(&g_progress_dir_index, prefix) && !str_hash_set_lookup(&g_progress_emitted, prefix)) { char* key = str_dup(prefix); if (key != NULL && array_list_add(g_progress_emitted_keys, key)) { str_hash_set_insert_ref(&g_progress_emitted, key); if (g_change_dirs_active) { const File* dir = progress_dir_lookup(prefix); if (dir != NULL) change_emit_dir_sent(config, dir); } else { char* escaped = output_escape(prefix, config->eight_bit_output); printf("%s/\n", escaped ? escaped : prefix); free(escaped); } g_progress_index++; } else { free(key); } } free(prefix); } } /* Feed a transferred entry's ancestor directories into the change-event stream * before the entry's own line, so -i/--out-format and --progress report * directories in rsync's depth-first order. Every directory is an ancestor of * some emitted entry (a file, symlink, special, hard link or the empty-directory * entry the scanner emits for a leaf), so this covers the whole tree. */ void client_change_emit_ancestors(const Config* config, const File* file) { if (config == NULL || file == NULL) return; if (!g_progress_active && !g_change_dirs_active) return; const char* rel = delete_display_path(config, file_wire_path(file)); client_progress_emit_ancestors(config, rel); } /* rsync's --info=name/progress line for one entry: transfer-relative name (a * trailing slash for directories) plus the ` -> target` symlink suffix. */ static char* progress_entry_line(const File* file, const char* rel) { const char* arrow = NULL; const char* target = NULL; if (file->is_symlink && file->symlink_target != NULL) { arrow = " -> "; target = file->symlink_target; } else if (file->link_group != 0 && !file->link_first && file->hardlink_target != NULL) { arrow = " => "; target = file->hardlink_target; } size_t rel_len = strlen(rel); bool dir_slash = file->is_dir && (rel_len == 0 || rel[rel_len - 1] != '/'); size_t extra = (dir_slash ? 1u : 0u) + (target != NULL ? 4u + strlen(target) : 0u); char* line = malloc(rel_len + extra + 1); if (line == NULL) return NULL; memcpy(line, rel, rel_len); size_t off = rel_len; if (dir_slash) line[off++] = '/'; if (target != NULL) { memcpy(line + off, arrow, 4); off += 4; memcpy(line + off, target, strlen(target)); off += strlen(target); } line[off] = '\0'; return line; } void client_progress_begin(const Config* config) { change_reset_name_root(); g_progress_active = progress_requested(config); g_progress_xferred = 0; g_progress_index = 1; /* the transfer root is file-list entry #0 */ if (!g_progress_active && !g_change_dirs_active) { /* `--info=flist` prints rsync's file-list header even without progress. */ if (!config->quiet && info_flag_enabled(config, LOG_INFO_FLIST)) { printf("sending incremental file list\n"); fflush(stdout); } return; } /* -i/--out-format alone do not print the header, but --progress always does and --info=flist does under any output mode (rsync prints it for `-i --info=flist` and `--out-format=... --info=flist` too). */ if (g_progress_active || (!config->quiet && info_flag_enabled(config, LOG_INFO_FLIST))) printf("sending incremental file list\n"); /* rsync prints the transfer-root directory before the first entry. Under -i/--out-format it is the root change line (`.d..t...... ./`); otherwise it is the plain --info=name / --progress name line. */ if (g_change_dirs_active) { const File* root = progress_dir_lookup(""); if (root != NULL) change_emit_dir_sent(config, root); } else { printf("./\n"); } fflush(stdout); } /* Emit the name (unless itemize/out-format already did) and the two progress * frames for one transferred regular file. */ void client_progress_file(const Config* config, const File* file) { if (!g_progress_active || file == NULL || !file->data) return; g_progress_xferred++; unsigned long long size = file->data->size; if (!config->itemize_changes && config->out_format == NULL) { const char* rel = delete_display_path(config, file_wire_path(file)); char* escaped = output_escape(rel, config->eight_bit_output); printf("%s\n", escaped ? escaped : (rel ? rel : "")); free(escaped); } clock_gettime(CLOCK_MONOTONIC, &g_progress_file_start); char frame[160]; progress_first_frame(size, frame, sizeof(frame)); fputs(frame, stdout); progress_final_frame(size, frame, sizeof(frame)); fputs(frame, stdout); g_progress_index++; fflush(stdout); } /* Emit the name line for a transferred non-regular entry (directory, symlink, * special or hard-link sibling): rsync prints these in the file list but has no * progress frame for them. */ void client_progress_name(const Config* config, const File* file) { if (!g_progress_active || file == NULL) return; const char* rel = delete_display_path(config, file_wire_path(file)); if (!config->itemize_changes && config->out_format == NULL) { char* line = progress_entry_line(file, rel ? rel : ""); if (line != NULL) { char* escaped = output_escape(line, config->eight_bit_output); printf("%s\n", escaped ? escaped : line); free(escaped); free(line); fflush(stdout); } } g_progress_index++; } /* An entry the receiver already had prints no name under --progress but still * occupies a file-list slot in the `to-chk` numerator. */ void client_progress_uptodate(const Config* config, const File* file) { (void)config; (void)file; if (!g_progress_active) return; g_progress_index++; } static bool progress_precount_add_dir(ProgressPrecount* p, const char* path) { if (path == NULL || path[0] == '\0') return true; char* dup = str_dup(path); if (dup == NULL) return false; if (array_list_add(p->dir_paths, dup)) return true; free(dup); return false; } static int progress_path_compare(const void* left, const void* right) { const char* const* a = (const char* const*)left; const char* const* b = (const char* const*)right; return strcmp(*a, *b); } /* Sort the collected directory paths and drop duplicates so a large * --files-from list (many entries sharing an implied ancestor) cannot grow the * list without bound. */ static void progress_precount_dedup_dirs(ArrayList* dir_paths) { if (dir_paths == NULL || dir_paths->size < 2) return; qsort(dir_paths->items, (size_t)dir_paths->size, sizeof(char*), progress_path_compare); int write = 0; for (int read = 0; read < dir_paths->size; read++) { char* current = (char*)dir_paths->items[read]; if (write > 0 && strcmp((char*)dir_paths->items[write - 1], current) == 0) { free(current); continue; } dir_paths->items[write++] = current; } dir_paths->size = write; } /* Create a metadata-bearing directory File for the transfer-relative directory * `rel` ("" is the transfer root), stat'ing it below config->send_directory. * The -d/--files-from dirs generator never traverses directories, so this * synthesizes the metadata the recursive scanner captures through * scanner_capture_dir_time, letting -i/--out-format render %M/%B/%U/%G and the * transfer-root/ancestor lines identically on both paths. Returns NULL when * the path cannot be stat'd as a directory or on allocation failure (the line * is then simply omitted, exactly as before). */ static File* progress_precount_make_dir(const Config* config, const char* rel) { if (config == NULL || config->send_directory == NULL) return NULL; char* fs_path = (rel == NULL || rel[0] == '\0') ? str_dup(config->send_directory) : path_cat(config->send_directory, rel); if (fs_path == NULL) return NULL; struct stat st; if (stat(fs_path, &st) != 0 || !S_ISDIR(st.st_mode)) { free(fs_path); return NULL; } File* file = file_create(fs_path); if (file == NULL) { free(fs_path); return NULL; } file->is_dir = true; file->metadata = file_metadata_create(fs_path, &st, config->preserve_atimes, config->preserve_crtimes); file->send_path = str_dup(rel != NULL ? rel : ""); free(fs_path); if (file->metadata == NULL || file->send_path == NULL) { file_destroy(file); return NULL; } return file; } /* The -d/--files-from dirs generator neither traverses nor records directories, * so its metadata walk captures no Files. Synthesize the transfer root and * every listed/implied directory from `entry_rels` so -i/--out-format emits the * same root and ancestor lines the recursive scan does. `root_emitted` is true * when the generator itself emits the root entry (bare `-d `), whose * data-pass line must not be duplicated. Returns false only on allocation * failure. */ static bool progress_precount_synthesize_dirs(const Config* config, ProgressPrecount* out, const ArrayList* entry_rels, bool root_emitted) { for (int i = 0; i < entry_rels->size; i++) { const char* rel = (const char*)entry_rels->items[i]; if (rel == NULL) continue; size_t len = strlen(rel); for (size_t j = 1; j < len; j++) { if (rel[j] != '/') continue; /* --no-implied-dirs: rsync neither creates nor itemizes an implied parent, so only explicitly listed directories get a line. */ if (config->no_implied_dirs) break; char* prefix = malloc(j + 1); if (prefix == NULL) return false; memcpy(prefix, rel, j); prefix[j] = '\0'; if (!progress_precount_add_dir(out, prefix)) { free(prefix); return false; } free(prefix); } } progress_precount_dedup_dirs(out->dir_paths); for (int i = 0; i < out->dir_paths->size; i++) { const char* rel = (const char*)out->dir_paths->items[i]; File* dir = progress_precount_make_dir(config, rel); if (dir == NULL) continue; if (!array_list_add(out->dir_files, dir)) { file_destroy(dir); return false; } } /* Emit the transfer root only when the generator actually emitted an entry: --prune-empty-dirs (or an empty --files-from list) transfers nothing, and rsync prints no root line then either. */ if (!root_emitted && entry_rels->size > 0) { File* root = progress_precount_make_dir(config, ""); if (root != NULL && !array_list_add(out->dir_files, root)) { file_destroy(root); return false; } } return true; } /* Metadata-only walk collecting the full file-list total and every directory * name. It uses its own scanner (fresh filter compilation and hard-link table) * so the data pass's link-group state is never perturbed. */ static bool progress_precount_scan(const Config* config, ProgressPrecount* out) { out->dir_paths = array_list_create(free); out->dir_files = array_list_create(file_destroy); out->dir_refs = array_list_create(dir_ref_destroy); if (out->dir_paths == NULL || out->dir_files == NULL || out->dir_refs == NULL) { progress_precount_dispose(out); return false; } out->total = 0; /* The -d/--files-from dirs generator never calls scanner_capture_dir_time, so the walk below captures no directory Files. Record every emitted entry's transfer-relative name so the implied ancestors can be synthesized once the walk is done. */ bool synthesize = g_change_dirs_active && config->dirs; ArrayList* entry_rels = synthesize ? array_list_create(free) : NULL; if (synthesize && entry_rels == NULL) { progress_precount_dispose(out); return false; } bool root_emitted = false; PreparedScanner prepared; memset(&prepared, 0, sizeof(prepared)); if (!prepare_scanner(config, 0, &prepared)) { array_list_delete(entry_rels); progress_precount_dispose(out); return false; } ScannerOptions local = prepared.options; local.list_dirs = true; local.note_nonreg = false; local.note_mount = false; local.dir_count = NULL; local.use_metadata = false; local.preserve_xattrs = false; local.preserve_acls = false; local.checksum = false; /* Capture one metadata-bearing File per traversed directory (including the transfer root) so -i/--out-format can render %M/%B/%U/%G for directories. */ local.capture_dir_times = true; local.excluded_paths = NULL; local.size_skipped_paths = NULL; local.synced_dirs = NULL; local.plan_dirs = NULL; local.dir_entries = out->dir_files; local.dir_entries_mutex = NULL; local.hardlinks = NULL; DirectoryScanner* scanner = directory_scanner_create_with_options(config->send_directory, &local); bool ok = scanner != NULL; if (scanner != NULL) { Chunk* chunk; while (ok && (chunk = directory_scanner_next(scanner)) != NULL) { out->total += (unsigned long long)chunk->element_count; for (int i = 0; i < chunk->element_count && ok; i++) { const File* f = chunk->items[i]; if (f == NULL) continue; const char* rel = delete_display_path(config, file_wire_path(f)); if (f->is_dir) { if (rel != NULL && rel[0] == '\0') root_emitted = true; ok = progress_precount_add_dir(out, rel); if (!ok) break; } if (synthesize && rel != NULL) { char* dup = str_dup(rel); if (dup == NULL || !array_list_add(entry_rels, dup)) { free(dup); ok = false; } } } chunk_destroy(chunk); } if (ok && directory_scanner_failed(scanner)) ok = false; directory_scanner_destroy(scanner); } prepared_scanner_destroy(&prepared); if (!ok) { array_list_delete(entry_rels); progress_precount_dispose(out); return false; } if (synthesize) { bool synth_ok = progress_precount_synthesize_dirs(config, out, entry_rels, root_emitted); array_list_delete(entry_rels); if (!synth_ok) { progress_precount_dispose(out); return false; } } /* Build the name -> File lookup from the captured directory Files. */ for (int i = 0; i < out->dir_files->size; i++) { File* f = (File*)out->dir_files->items[i]; if (f == NULL) continue; if (!progress_precount_add_ref(out, config, f)) { progress_precount_dispose(out); return false; } } if (out->dir_refs->size > 1) qsort(out->dir_refs->items, (size_t)out->dir_refs->size, sizeof(DirRef*), dir_ref_compare); out->total += 1; /* the transfer root "." */ return true; } /* Reuse the --delete-during/--delete-delay keep-set pre-scan: its traversed * directory list already holds every directory and `non_dir_count` the entries * counted during that same pass, so progress costs no second walk. */ static bool progress_precount_from_plan_dirs(const Config* config, const ArrayList* plan_dirs, unsigned long long non_dir_count, ProgressPrecount* out) { out->dir_paths = array_list_create(free); out->dir_files = array_list_create(file_destroy); out->dir_refs = array_list_create(dir_ref_destroy); if (out->dir_paths == NULL || out->dir_files == NULL || out->dir_refs == NULL) { progress_precount_dispose(out); return false; } out->total = non_dir_count + 1; /* The delete pre-scan's plan list omits the transfer root, so synthesize its entry here; it is only used for the root change line. */ File* root = file_create(""); if (root == NULL || !array_list_add(out->dir_files, root)) { file_destroy(root); progress_precount_dispose(out); return false; } root->is_dir = true; if (!progress_precount_add_ref(out, config, root)) { progress_precount_dispose(out); return false; } for (int i = 0; i < plan_dirs->size; i++) { const char* path = (const char*)plan_dirs->items[i]; const char* rel = config->send_directory != NULL ? utils_strip_transfer_root(path, config->send_directory) : path; if (!progress_precount_add_dir(out, rel)) { progress_precount_dispose(out); return false; } File* dir = file_create(""); if (dir == NULL) { progress_precount_dispose(out); return false; } dir->is_dir = true; dir->send_path = str_dup(rel != NULL ? rel : ""); if (dir->send_path == NULL || !array_list_add(out->dir_files, dir)) { file_destroy(dir); progress_precount_dispose(out); return false; } if (!progress_precount_add_ref(out, config, dir)) { progress_precount_dispose(out); return false; } } if (out->dir_refs->size > 1) qsort(out->dir_refs->items, (size_t)out->dir_refs->size, sizeof(DirRef*), dir_ref_compare); out->total += (unsigned long long)out->dir_paths->size; return true; } /* Build the optional progress pre-count. A failed pre-count is non-fatal: the * transfer proceeds and the progress denominator falls back to the transferred * file count. */ void client_progress_prepare(const Config* config, const ArrayList* plan_dirs, unsigned long long plan_non_dir_count) { client_progress_cleanup(); g_progress_active = progress_requested(config); g_change_dirs_active = config->itemize_changes || config->out_format != NULL; if (!g_progress_active && !g_change_dirs_active) return; /* -i/--out-format render directory metadata (%M/%B/%U/%G) that only the metadata walk captures; the --delete-during/--delete-delay plan list has no metadata, so prefer the walk whenever a change line is rendered. Pure --progress keeps reusing the plan list and its cheaper path-only pass. */ bool ok = (plan_dirs != NULL && !g_change_dirs_active) ? progress_precount_from_plan_dirs(config, plan_dirs, plan_non_dir_count, &g_progress_precount) : progress_precount_scan(config, &g_progress_precount); if (!ok) { g_progress_total = 0; return; } g_progress_total = g_progress_precount.total; if (g_progress_precount.dir_paths != NULL && g_progress_precount.dir_paths->size > 0 && path_index_build(&g_progress_dir_index, (const char* const*)g_progress_precount.dir_paths->items, (size_t)g_progress_precount.dir_paths->size)) g_progress_dir_index_valid = true; if (str_hash_set_init(&g_progress_emitted, (size_t)(g_progress_precount.dir_paths != NULL ? g_progress_precount.dir_paths->size + 1 : 1))) g_progress_emitted_valid = true; g_progress_emitted_keys = array_list_create(free); } /* Read the optional STATUS_STATS record (protocol 2.25.0) that the receiver * sends just before its terminal status when report_stats was negotiated. * Consumes the would-delete path list into `would_delete` (optional). */ bool receive_stats_record(int fd, ReceiverStats* stats, ArrayList* would_delete) { if (!format_stats_receive(fd, stats)) return false; int count = 0; if (!receive_int(fd, &count) || count < 0 || count > MAX_MANIFEST_ENTRIES) return false; /* Mirror the delete-plan parser: every retained path must be a valid destination-relative path, and the whole list shares one MAX_MANIFEST_BYTES budget so a hostile peer cannot make the client retain unbounded memory. */ size_t bytes = 0; for (int i = 0; i < count; i++) { char* path = receive_wire_str(fd); if (!path) return false; if (path[0] == '\0' || path[0] == '/' || has_path_traversal(path)) { free(path); return false; } if (would_delete) { size_t entry_size = strlen(path) + sizeof(char*) + 16; if (entry_size > MAX_MANIFEST_BYTES - bytes) { free(path); return false; } bytes += entry_size; if (!array_list_add(would_delete, path)) { free(path); return false; } } else { free(path); } } return true; } /* Strip the transfer-root prefix from a receiver-reported destination-relative * delete path so a `*deleting` line matches rsync's transfer-relative name * (FastSync's destination mirror includes the source's absolute path). */ const char* delete_display_path(const Config* config, const char* path) { if (!config || !path || !config->send_directory) return path; return utils_strip_transfer_root(path, config->send_directory); }