refactor(config): single X-macro table for serialized fields

Every Config field that crosses the wire was declared in up to six places
(struct member, config_set_defaults, send_*, receive_*, and the two CLI
option tables) and could drift silently.  Add CONFIG_WIRE_FIELDS in
config.h: one ordered per-segment table where each serialized field is
declared once with its C type, default and wire codec (KIND).

config.h now expands the table to declare the struct members;
config_set_defaults() expands it to assign the defaults; and
config_send_wire_block()/config_receive() expand the per-segment lists to
emit/consume the frame.  The per-segment function names, call order and
segment boundaries are preserved exactly.

Fields with genuinely custom logic keep dedicated helpers but are still
declared once in the table: the protocol-version handshake (HEADER), daemon
SCRAM auth (STR_REDACTED_AUTH), the daemon module name (STR_MODULE), the
repeated count+array blocks (BLOCK_SKIP_SUFFIXES/BLOCK_BASIS/BLOCK_IDMAP),
--copy-as presence/ids (COPY_AS_*), and the derived --delta / use_xattrs
bits (DERIVED_DELTA, BOOL_XATTR_DERIVE).  The version field remains a
special header (validated before any other field is parsed) and is sent by
config_send_wire_block() explicitly.

No public field is renamed and PROTOCOL_VERSION stays "2.20.0".  Because
the struct declaration order is no longer the wire order, the wire order is
now enforced solely by the table and by a byte-exact golden test
(follow-up commit).  Add config_send_wire_block() so that test can hash the
frame body without the STATUS_OK handshake.
This commit is contained in:
2026-09-13 10:28:26 +02:00
parent 9242e86772
commit 87f6cb0243
2 changed files with 735 additions and 866 deletions
+368 -596
View File
File diff suppressed because it is too large Load Diff
+366 -269
View File
@@ -75,87 +75,203 @@ typedef struct {
* privilege_super_mode_permitted() in identity.h. */ * privilege_super_mode_permitted() in identity.h. */
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode; 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.20.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
* the struct member, config_set_defaults() expands it to assign the default,
* and config_send_wire_block()/config_receive_with_validate() expand the
* per-segment lists to emit/consume the frame in exactly this order. Do NOT
* reorder entries and do NOT change a field's segment/KIND without a
* PROTOCOL_VERSION bump: the resulting byte stream is pinned by
* test_config_wire_golden().
*
* Entry layout: X(MEMBER, CTYPE, DEFAULT, KIND)
* MEMBER struct member name (public; never rename)
* CTYPE C type of the member
* DEFAULT default-value expression used by config_set_defaults()
* KIND wire codec, dispatched to CONFIG_SEND_<KIND>/CONFIG_RECV_<KIND>
* in config.c (strings receive through a ConfigStringBudget).
*
* Fields with genuinely custom logic keep dedicated helpers but are still
* declared here exactly once: the protocol-version handshake (HEADER), the
* daemon SCRAM auth username (STR_REDACTED_AUTH), the daemon module name
* (STR_MODULE), repeated count+array blocks (BLOCK_*), --copy-as presence
* (COPY_AS_*), and the derived --delta / use_xattrs bits (DERIVED_DELTA,
* BOOL_XATTR_DERIVE).
* =========================================================================== */
#define CONFIG_WIRE_HEADER_FIELDS(X) X(version, char*, str_dup(PROTOCOL_VERSION), STR)
#define CONFIG_WIRE_CORE_FIELDS(X) \
X(eight_bit_output, bool, false, BOOL_8BIT) \
X(max_alloc, unsigned long long, DEFAULT_MAX_ALLOC, RAW_MAXALLOC) \
X(send_directory, char*, NULL, STR) \
X(receive_root_directory, char*, NULL, STR) \
X(save_to_disk, bool, false, BOOL) \
X(use_multithreading, bool, false, BOOL) \
X(use_chunk_serialization, bool, false, BOOL) \
X(use_compression, bool, false, BOOL) \
X(use_metadata, bool, false, BOOL) \
X(use_executability, bool, false, BOOL) \
X(compression_level, int, 5, INT) \
X(chunk_size, unsigned long long, DEFAULT_CHUNK_SIZE, RAW) \
X(use_sendfile, bool, false, BOOL)
#define CONFIG_WIRE_DELTA_FIELDS(X) \
X(use_delete, bool, false, BOOL) \
X(use_incremental, bool, false, BOOL) \
X(size_only, bool, false, BOOL) \
X(ignore_times, bool, false, BOOL) \
X(use_delta, bool, false, DERIVED_DELTA) \
X(delta_block_size, uint32_t, DELTA_BLOCK_SIZE_DEFAULT, RAW) \
X(delta_max_file_size, unsigned long long, DELTA_MAX_FILE_SIZE, RAW)
#define CONFIG_WIRE_FILE_OPTIONS_FIELDS(X) \
X(backup, bool, false, BOOL) \
X(backup_dir, char*, NULL, STR_OPT) \
X(remove_source_files, bool, false, BOOL) \
X(follow_symlinks, bool, false, BOOL) \
X(copy_links, bool, false, BOOL) \
X(safe_links, bool, false, BOOL) \
X(copy_unsafe_links, bool, false, BOOL) \
X(preserve_hard_links, bool, false, BOOL) \
X(preserve_acls, bool, false, BOOL) \
X(preserve_xattrs, bool, false, BOOL) \
X(preserve_devices, bool, false, BOOL) \
X(preserve_sparse, bool, false, BOOL) \
X(preserve_specials, bool, false, BOOL) \
X(copy_devices, bool, false, BOOL) \
X(write_devices, bool, false, BOOL)
#define CONFIG_WIRE_SELECTION_FIELDS(X) \
X(ignore_existing, bool, false, BOOL) \
X(existing, bool, false, BOOL) \
X(update, bool, false, BOOL) \
X(inplace, bool, false, BOOL) \
X(delay_updates, bool, false, BOOL) \
X(append, bool, false, BOOL) \
X(use_fsync, bool, false, BOOL) \
X(append_verify, bool, false, BOOL) \
X(delete_excluded, bool, false, BOOL) \
X(force_delete, bool, false, BOOL) \
X(delete_missing_args, bool, false, BOOL) \
X(delete_after, bool, false, BOOL) \
X(preallocate, bool, false, BOOL) \
X(max_delete, int, -1, RAW) \
X(relative, bool, false, BOOL) \
X(prune_empty_dirs, bool, false, BOOL) \
X(mkpath, bool, false, BOOL) \
X(delete_during, bool, false, BOOL) \
X(delete_delay, bool, false, BOOL)
#define CONFIG_WIRE_RESUME_FIELDS(X) \
X(temp_dir, char*, NULL, STR_OPT) \
X(partial, bool, false, BOOL) \
X(partial_dir, char*, NULL, STR_OPT) \
X(suffix, char*, NULL, STR_OPT) \
X(delete_before, bool, false, BOOL) \
X(checksum, bool, false, BOOL) \
X(modify_window, int, 0, RAW) \
X(compress_choice, char*, NULL, STR_KEEP) \
X(chmod_spec, char*, NULL, STR_KEEP) \
X(skip_compress_set, bool, false, BOOL) \
X(skip_compress_count, int, 0, INT_SKIPCOUNT) \
X(skip_compress_suffixes, char**, NULL, BLOCK_SKIP_SUFFIXES)
#define CONFIG_WIRE_BASIS_FIELDS(X) \
X(basis_count, int, 0, INT_BASISCOUNT) \
X(basis_dirs, BasisDest*, NULL, BLOCK_BASIS)
#define CONFIG_WIRE_FUZZY_FIELDS(X) X(fuzzy, bool, false, BOOL)
#define CONFIG_WIRE_CHECKSUM_FIELDS(X) \
X(checksum_algo, int, CHECKSUM_ALGO_XXH64, INT_CHECKSUM_ALGO) \
X(checksum_seed, uint64_t, 0, RAW)
#define CONFIG_WIRE_IDENTITY_FIELDS(X) \
X(numeric_ids, bool, false, BOOL) \
X(chown_uid_set, bool, false, BOOL) \
X(chown_uid, int32_t, 0, INT_IDENTITY) \
X(chown_gid_set, bool, false, BOOL) \
X(chown_gid, int32_t, 0, INT_IDENTITY) \
X(usermap_count, int, 0, INT_IDMAPCOUNT) \
X(usermap, IdentityMap*, NULL, BLOCK_IDMAP) \
X(groupmap_count, int, 0, INT_IDMAPCOUNT) \
X(groupmap, IdentityMap*, NULL, BLOCK_IDMAP)
#define CONFIG_WIRE_METADATA_TIMES_FIELDS(X) \
X(preserve_atimes, bool, false, BOOL) \
X(preserve_crtimes, bool, false, BOOL) \
X(omit_dir_times, bool, false, BOOL) \
X(omit_link_times, bool, false, BOOL)
#define CONFIG_WIRE_SYMLINK_TRUST_FIELDS(X) \
X(munge_links, bool, false, BOOL) \
X(keep_dirlinks, bool, false, BOOL)
#define CONFIG_WIRE_XATTR_FIELDS(X) X(fake_super, bool, false, BOOL_XATTR_DERIVE)
#define CONFIG_WIRE_MODULE_FIELDS(X) X(module, char*, NULL, STR_MODULE)
#define CONFIG_WIRE_DAEMON_AUTH_FIELDS(X) X(auth_user, char*, NULL, STR_REDACTED_AUTH)
#define CONFIG_WIRE_ICONV_FIELDS(X) X(iconv_spec, char*, NULL, STR_OPT)
#define CONFIG_WIRE_PRIVILEGE_FIELDS(X) X(super_mode, SuperMode, SUPER_MODE_AUTO, SUPERMODE)
#define CONFIG_WIRE_COPY_AS_FIELDS(X) \
X(copy_as_set, bool, false, COPY_AS_PRESENCE) \
X(copy_as_uid, int32_t, 0, COPY_AS_ID) \
X(copy_as_gid, int32_t, 0, COPY_AS_ID)
/* All serialized fields, in exact wire order. Concatenating the per-segment
* lists here is what keeps the declaration order = the wire order. */
#define CONFIG_WIRE_FIELDS(X) \
CONFIG_WIRE_HEADER_FIELDS(X) \
CONFIG_WIRE_CORE_FIELDS(X) \
CONFIG_WIRE_DELTA_FIELDS(X) \
CONFIG_WIRE_FILE_OPTIONS_FIELDS(X) \
CONFIG_WIRE_SELECTION_FIELDS(X) \
CONFIG_WIRE_RESUME_FIELDS(X) \
CONFIG_WIRE_BASIS_FIELDS(X) \
CONFIG_WIRE_FUZZY_FIELDS(X) \
CONFIG_WIRE_CHECKSUM_FIELDS(X) \
CONFIG_WIRE_IDENTITY_FIELDS(X) \
CONFIG_WIRE_METADATA_TIMES_FIELDS(X) \
CONFIG_WIRE_SYMLINK_TRUST_FIELDS(X) \
CONFIG_WIRE_XATTR_FIELDS(X) \
CONFIG_WIRE_MODULE_FIELDS(X) \
CONFIG_WIRE_DAEMON_AUTH_FIELDS(X) \
CONFIG_WIRE_ICONV_FIELDS(X) \
CONFIG_WIRE_PRIVILEGE_FIELDS(X) \
CONFIG_WIRE_COPY_AS_FIELDS(X)
typedef struct Config { typedef struct Config {
char* version;
char* send_directory;
char* receive_root_directory;
bool save_to_disk;
bool use_multithreading;
/* -j/--threads=N: number of parallel scanner worker threads for the -m /* -j/--threads=N: number of parallel scanner worker threads for the -m
* pipeline. 0 (the default, also set by bare -j/--threads) means "use the * pipeline. 0 (the default, also set by bare -j/--threads) means "use the
* scanner's built-in default" (4). CLIENT-ONLY: it is a local scheduling * scanner's built-in default" (4). CLIENT-ONLY: it is a local scheduling
* concern and is NEVER serialized into the wire config frame. */ * concern and is NEVER serialized into the wire config frame. */
int scanner_threads; int scanner_threads;
bool use_chunk_serialization;
bool use_compression;
bool use_sendfile;
bool use_metadata;
bool use_executability;
bool metadata_explicitly_disabled; bool metadata_explicitly_disabled;
bool show_progress; bool show_progress;
bool dry_run; bool dry_run;
bool remove_source_files;
bool use_delete;
int compression_level;
int compression_threads; int compression_threads;
unsigned long long chunk_size;
int ssh_port; int ssh_port;
TransportType transport; TransportType transport;
char* ssh_destination; char* ssh_destination;
/* Daemon module selection (Wave A, protocol 2.15.0). Client-composed from a
* host::module/path destination; NULL or "" means "no module" (the ordinary
* standalone-server path). Crosses the wire as a trailing config-frame
* string so the daemon can look the module up in its own config and confine
* the connection to the module's root (never a client-chosen root). */
char* module;
/* Daemon password authentication (A7 remediation, protocol 2.19.0).
* Client-composed from a --password-file whose first meaningful line is
* `user:password`: the client sends ONLY the username in the config frame
* (auth_user); the literal password is kept in auth_password CLIENT-SIDE for
* the duration of the SCRAM challenge/response and is NEVER serialized. Both
* are NULL when the client has no credentials to present; a module WITHOUT
* `auth users` stays open and the server ignores any credentials that do
* arrive (the client sends them opportunistically and the server decides). */
char* auth_user;
char* auth_password; char* auth_password;
/* Client-only path of --password-file (never crosses the wire; it is read to /* Client-only path of --password-file (never crosses the wire; it is read to
* populate auth_user/auth_password before connecting). */ * populate auth_user/auth_password before connecting). */
char* password_file; char* password_file;
char* fastsync_server_path; char* fastsync_server_path;
/* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the
* charset of FILE NAMES at the wire boundary. CONVERT_SPEC is
* "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is
* the remote side's charset and defaults to LOCAL. The sender converts
* every path LOCAL->REMOTE before transmitting it; the receiver converts
* every received path back REMOTE->LOCAL before creating/writing it. The
* FULL SPEC crosses the wire as a trailing config-frame string so each end
* derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL
* (or "") means no conversion: identity with zero overhead. See charset.c
* and the PROTOCOL_VERSION note below. */
char* iconv_spec;
char** exclude_patterns; char** exclude_patterns;
int exclude_count; int exclude_count;
char** include_patterns; char** include_patterns;
int include_count; int include_count;
unsigned long long max_size; unsigned long long max_size;
unsigned long long min_size; unsigned long long min_size;
unsigned long long max_alloc;
bool use_incremental;
bool ignore_times;
bool size_only;
bool use_delta;
bool whole_file; bool whole_file;
/* -y/--fuzzy: when a file must be transferred and the destination holds no
* usable file at the exact path, the receiver may reuse a SIMILAR-named
* existing regular file in the same destination directory as the delta
* basis so the sender transmits only the differences. Crosses the wire
* (the receiver performs the candidate search); the CLI implies
* --incremental + --delta because the similar-basis only matters on the
* receiver-driven delta path. Off by default. */
bool fuzzy;
int modify_window;
uint32_t delta_block_size;
unsigned long long delta_max_file_size;
bool use_tls; bool use_tls;
char* server_host; char* server_host;
int server_port; int server_port;
@@ -170,18 +286,10 @@ typedef struct Config {
/* --contimeout: connect()/accept timeout, transport layer only. */ /* --contimeout: connect()/accept timeout, transport layer only. */
int contimeout; int contimeout;
bool quiet; bool quiet;
bool backup;
char* backup_dir;
bool stats; bool stats;
int max_depth; int max_depth;
FILE* log_file; FILE* log_file;
bool follow_symlinks;
bool partial;
// Issue #120: Symlink handling
bool copy_links;
bool safe_links;
bool copy_unsafe_links;
/* Phase 4 symlink-trust. -k/--copy-dirlinks and --munge-links are /* Phase 4 symlink-trust. -k/--copy-dirlinks and --munge-links are
* CLIENT/sender-side only (they decide how the SENDER scans and rewrites * CLIENT/sender-side only (they decide how the SENDER scans and rewrites
* symlinks; the receiver never reads them), so they never cross the wire. * symlinks; the receiver never reads them), so they never cross the wire.
@@ -189,31 +297,6 @@ typedef struct Config {
* symlink-to-directory as a directory) and CROSSES the wire along with * symlink-to-directory as a directory) and CROSSES the wire along with
* --munge-links (so the receiver knows to unmunge). */ * --munge-links (so the receiver knows to unmunge). */
bool copy_dirlinks; /* client-only, sender-side (-k) */ bool copy_dirlinks; /* client-only, sender-side (-k) */
bool munge_links; /* crosses the wire */
bool keep_dirlinks; /* crosses the wire (-K) */
// Issue #121: Extended metadata preservation
bool preserve_hard_links;
bool preserve_acls;
bool preserve_xattrs;
bool preserve_devices;
bool preserve_sparse;
/* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device
* nodes on the destination by recreating them (mknod/mkfifo) instead of
* transferring content. preserve_specials mirrors rsync --specials (the
* special-file half of -D); preserve_devices mirrors --devices (the device
* half of -D); both CROSS the wire so the receiver knows a special/device
* entry must be recreated rather than written as a regular file. */
bool preserve_specials;
/* --copy-devices: copy the CONTENT of a source device as an ordinary regular
* file on the destination (rsync's non-privileged safe mode), instead of
* recreating the device node. CROSSES the wire (receiver treats the entry as
* a regular file, which is the default, so this is belt-and-braces). */
bool copy_devices;
/* --write-devices: write the received data directly INTO an existing device
* node on the destination instead of creating a regular file. Dangeroud;
* see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */
bool write_devices;
// Issue #122: Output/logging options // Issue #122: Output/logging options
bool itemize_changes; bool itemize_changes;
@@ -223,57 +306,17 @@ typedef struct Config {
int debug_level; int debug_level;
bool list_only; bool list_only;
bool human_readable; bool human_readable;
bool eight_bit_output;
// Issue #127: Transfer modes
bool existing;
bool ignore_existing;
bool update;
bool inplace;
bool delay_updates;
bool use_fsync;
bool append;
bool append_verify;
/* --preallocate: allocates the destination file's full expected space up
* front (before any data is written) so a transfer that would overflow disk
* fails fast at allocation time and the file is laid out contiguously,
* avoiding fragmentation. Receiver-side, crosses the wire. */
bool preallocate;
// Issue #128: Extended delete options
/* --delete-excluded: also delete destination entries that were excluded on
* the source. Default (off) matches rsync: excluded paths are protected from
* deletion. Crosses the wire (the sender encodes the choice by whether it
* transmits a protected-prefix list with the keep-set manifest). */
bool delete_excluded;
bool delete_after;
/* --max-delete=NUM: the receiver refuses to delete more than NUM entries per
* run (all-or-nothing: when the extras would exceed NUM nothing is removed and
* the transfer fails with a distinct error). -1 == no client limit (the
* server hard bound MAX_SERVER_DELETE_COUNT still applies). */
int max_delete;
/* --ignore-errors (client-only, never serialized): a sender-side source I/O /* --ignore-errors (client-only, never serialized): a sender-side source I/O
* error (an unreadable directory during the scan) normally aborts the run so * error (an unreadable directory during the scan) normally aborts the run so
* no deletion happens; with --ignore-errors the scan continues and the * no deletion happens; with --ignore-errors the scan continues and the
* (partial) keep-set is still transmitted so the deletion runs. */ * (partial) keep-set is still transmitted so the deletion runs. */
bool ignore_errors; bool ignore_errors;
/* --force (receiver-side): a regular file may replace a destination
* directory by removing that (possibly non-empty, symlink-safe) directory
* tree first, instead of failing the write. Crosses the wire. */
bool force_delete;
/* --ignore-missing-args (client-only, never serialized): a --files-from /* --ignore-missing-args (client-only, never serialized): a --files-from
* entry that does not exist under the source is silently skipped instead of * entry that does not exist under the source is silently skipped instead of
* failing the run. Sender-side only: nothing is sent for it and it never * failing the run. Sender-side only: nothing is sent for it and it never
* enters the keep-set. Implied by --delete-missing-args. */ * enters the keep-set. Implied by --delete-missing-args. */
bool ignore_missing_args; bool ignore_missing_args;
/* --delete-missing-args: implies --ignore-missing-args; additionally each
* missing entry's destination mirror (computed like a present entry's wire
* path) is deleted receiver-side. Crosses the wire and is gated by the
* server's --allow-delete policy like --delete. rsync-parity: independent
* of ordinary --delete processing (it does not imply --delete); a non-empty
* directory mirror is only removed with --force or --delete in effect, and
* the missing-args deletions are not counted toward --max-delete. */
bool delete_missing_args;
// Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are // Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are
// never serialized to the wire (the receiver must not learn them). // never serialized to the wire (the receiver must not learn them).
@@ -283,23 +326,14 @@ typedef struct Config {
bool from0; /* -0/--from0: NUL-delimited *-from files */ bool from0; /* -0/--from0: NUL-delimited *-from files */
bool cvs_exclude; /* -C/--cvs-exclude: standard CVS ignore set */ bool cvs_exclude; /* -C/--cvs-exclude: standard CVS ignore set */
bool per_dir_filter; /* -F: apply per-directory .rsync-filter files */ bool per_dir_filter; /* -F: apply per-directory .rsync-filter files */
bool prune_empty_dirs;
bool one_file_system; /* -x/--one-file-system: do not cross filesystem boundaries */ bool one_file_system; /* -x/--one-file-system: do not cross filesystem boundaries */
/* -R/--relative: crosses the wire; with --files-from listed entries keep
* their bare relative destination path (no source-root mirror prefix). */
bool relative;
/* --no-implied-dirs: client-only. With -R + --files-from, refuse to place a /* --no-implied-dirs: client-only. With -R + --files-from, refuse to place a
* listed file whose ancestor directory is not itself explicitly listed. */ * listed file whose ancestor directory is not itself explicitly listed. */
bool no_implied_dirs; bool no_implied_dirs;
/* -d/--dirs: client-only. Transfer the directory entries named by the /* -d/--dirs: client-only. Transfer the directory entries named by the
* source argument / --files-from list without recursing into contents. */ * source argument / --files-from list without recursing into contents. */
bool dirs; bool dirs;
/* --mkpath: crosses the wire. Tells the server to create the destination
* root directory (and missing leading components below its authorized root)
* at connection start instead of requiring it to already exist. */
bool mkpath;
// Issue #130: Remote shell/connection options
/* -e/--rsh: the remote-shell program used to establish the SSH transport. /* -e/--rsh: the remote-shell program used to establish the SSH transport.
* NULL means the default "ssh". Client-only launch concern: NEVER crosses * NULL means the default "ssh". Client-only launch concern: NEVER crosses
* the wire (it is not meaningful to the daemon/server handshake). */ * the wire (it is not meaningful to the daemon/server handshake). */
@@ -312,7 +346,6 @@ typedef struct Config {
* concern: NEVER crosses the wire. */ * concern: NEVER crosses the wire. */
int outbuf; int outbuf;
bool old_args; bool old_args;
char* temp_dir;
/* --remote-option=OPT (Phase 5, long form only): one or more extra command-line /* --remote-option=OPT (Phase 5, long form only): one or more extra command-line
* options to append to the REMOTE server invocation over SSH. CLIENT-ONLY: * options to append to the REMOTE server invocation over SSH. CLIENT-ONLY:
* they are composed into the remote command line by ssh_build_remote_command() * they are composed into the remote command line by ssh_build_remote_command()
@@ -321,34 +354,6 @@ typedef struct Config {
* do NOT cross the wire and are never parsed on the receiver process. */ * do NOT cross the wire and are never parsed on the receiver process. */
char** remote_options; char** remote_options;
int remote_option_count; int remote_option_count;
/* Alternate basis directories, ordered by command-line appearance. Each
* entry's type selects compare/copy/link behavior on an exact match. These
* cross the wire so the receiver can consult them; they are interpreted
* relative to the destination root and confined there. */
BasisDest* basis_dirs;
int basis_count;
// PR #174: Partial transfer resumption
char* partial_dir;
// PR #178: Backup versioning
char* suffix;
// PR #179: Delete policies
bool delete_before;
/* rsync deletion-timing family (real from Phase 3). At most one of
delete_before / delete_during / delete_delay / delete_after may be set, and
only together with use_delete (the CLI implies --delete for each of them).
delete_before and delete_during select the EARLY engine mode: the keep-set
manifest is transmitted before any file data and extras are removed then,
acknowledged, before the first data byte. delete_delay and delete_after
select the LATE commit mode: extras are removed only after the whole
transfer has succeeded (plain --delete keeps this mode). The exact
semantics and the divergences from rsync are documented in RSYNC_COMPAT.md
and in config_delete_timing_early() below. */
bool delete_during;
bool delete_delay;
// PR #181: IPv6 and bind address // PR #181: IPv6 and bind address
char* address; char* address;
@@ -370,119 +375,19 @@ typedef struct Config {
* MOTD is shown when a daemon offers one). */ * MOTD is shown when a daemon offers one). */
bool no_motd; bool no_motd;
// PR #183: Checksum comparison
bool checksum;
// PR #184: Compression algorithm negotiation
char* compress_choice;
char* chmod_spec;
/* --checksum-choice / --cc and --checksum-seed. checksum_algo is the id of
* the whole-file content-digest algorithm used by the per-file --incremental
* handshake (sender computes it, receiver compares it to skip unchanged
* files) and by the basis-dir content verification. checksum_seed is passed
* to xxHash64 (and to the delta block strong hash, low 32 bits); md5 has no
* seed so it is ignored there. Both cross the wire: the receiver MUST hash
* the on-disk old file with the same algorithm and seed to reach a matching
* digest. Defaults (XXH64 / seed 0) reproduce the pre-existing behavior
* byte-for-byte. */
int checksum_algo; /* ChecksumAlgo, default CHECKSUM_ALGO_XXH64 */
uint64_t checksum_seed; /* default 0 */
char** skip_compress_suffixes;
int skip_compress_count;
bool skip_compress_set;
// Issue #131: Identity mapping. These configure whether and how the receiver
// applies ownership when it is actually preserved/applied. ALL of them cross
// the wire (protocol 2.11.0) so the receiver resolves and applies ownership
// with the exact policy the client requested. Plain -M/--preserve still does
// NOT apply ownership (FastSync's deliberate conservative default); it is
// only attempted when at least one of these is set (see identity.h).
/* --numeric-ids: no name lookup, use the transmitted numeric ids raw. */
bool numeric_ids;
/* --chown USER (owner) override; IDENTITY_CURRENT = the receiver's euid. */
bool chown_uid_set;
int32_t chown_uid;
/* --chown :GROUP (group) override; IDENTITY_CURRENT = the receiver's egid. */
bool chown_gid_set;
int32_t chown_gid;
/* --usermap / --groupmap entries, in order (first match wins). */
IdentityMap* usermap;
int usermap_count;
IdentityMap* groupmap;
int groupmap_count;
/* --super / --no-super (P7 Wave E, protocol 2.18.0): receiver-side privilege
* policy for super-user activities confined below the authorized receive
* root. SUPER_MODE_AUTO (default) preserves the pre-existing best-effort
* behavior: the confined super-user operation is ALWAYS attempted and an
* unprivileged attempt is refused by the kernel and skipped per entry.
* SUPER_MODE_ON (--super) explicitly REQUESTS those activities (char/block
* device-node creation, --write-devices); it does NOT imply --numeric-ids and
* never enables ownership application on its own. SUPER_MODE_OFF
* (--no-super) FORBIDS them even when running as root. FastSync NEVER
* elevates privileges (no setuid/seteuid/setgid) and never bypasses the
* fd-relative confinement (file_open_secure_parent, O_NOFOLLOW, root checks);
* --super only permits an attempt that is already confined. Crosses the wire
* as a trailing int so the receiver can enforce the policy. See
* privilege_super_permitted() and identity_ownership_requested() in
* identity.h. */
SuperMode super_mode;
// Receiver-side runtime staging registry for --delay-updates. Never sent // Receiver-side runtime staging registry for --delay-updates. Never sent
// over the wire and never set on the sender side. // over the wire and never set on the sender side.
DelayUpdatesContext* delay_context; DelayUpdatesContext* delay_context;
// Phase 4: metadata time preservation. -U/--atimes and -N/--crtimes capture
// and transmit the source access / birth time (both sender and receiver
// effect, so they CROSS the wire). --omit-dir-times/-O and
// --omit-link-times/-J are receiver-side prefs (CROSS the wire). Their
// exact capture/transmit/apply semantics are documented in RSYNC_COMPAT.md.
/* -U/--atimes: preserve source access times on the destination. */
bool preserve_atimes;
/* -N/--crtimes: capture+transmit source birth time; see RSYNC_COMPAT for the
* receiver not-applied divergence. */
bool preserve_crtimes;
/* -O/--omit-dir-times: do not apply mtimes to directories. */
bool omit_dir_times;
/* -J/--omit-link-times: do not apply times to symlinks. */
bool omit_link_times;
/* --open-noatime: CLIENT-ONLY (never crosses the wire). The sender opens /* --open-noatime: CLIENT-ONLY (never crosses the wire). The sender opens
* source files with O_NOATIME so reading for transfer does not bump the * source files with O_NOATIME so reading for transfer does not bump the
* source access time. */ * source access time. */
bool open_noatime; bool open_noatime;
// Phase 4: xattr / ACL / fake-super preservation.
/* -X/--xattrs and -A/--acls toggle the sender's capture and the receiver's
* application of per-file extended attributes (xattrs). Both cross the wire:
* the sender only transmits the bounded, whitelisted attribute set it
* captures and the receiver re-validates namespaces/sizes before applying
* fd-relative. With neither set (the default) no xattr block is sent, so the
* wire is byte-identical to prior protocol versions for unaffected runs. */
/* true when preserve_xattrs || preserve_acls; the sender/receiver gate the /* true when preserve_xattrs || preserve_acls; the sender/receiver gate the
* xattr wire block on this single flag. */ * xattr wire block on this single flag. */
bool use_xattrs; bool use_xattrs;
/* --fake-super: receiver-only. When set, each written file additionally gets
* a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime
* so a later privileged restore could re-apply them. Crosses the wire. */
bool fake_super;
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset
* implementation, a documented divergence from rsync's real identity switch:
* the receiver does NOT change its process credentials (FastSync's receiver
* is multithreaded, so a setuid/seteuid drop would be unsafe). Instead the
* receiver FORCES the ownership of every entry it writes to copy_as_uid /
* copy_as_gid through the existing confined, fd-relative identity path
* (fchown/fchownat), which REQUIRES receiver privilege (root); an
* unprivileged receiver REFUSES the whole transfer up front at the config
* handshake (never a silent wrong-ownership result). All three fields CROSS
* the wire as a trailing config-frame block so the receiver learns the
* requested ids; see the PROTOCOL_VERSION note below. */
bool copy_as_set;
int32_t copy_as_uid;
int32_t copy_as_gid;
// Phase 5: --trust-sender
/* Long-form-only, receiver-local policy. rsync's --trust-sender tells the /* Long-form-only, receiver-local policy. rsync's --trust-sender tells the
* receiving side to trust that the sender already produced a sane file list, * receiving side to trust that the sender already produced a sane file list,
* relaxing the receiver's own up-front re-validation of every incoming path. * relaxing the receiver's own up-front re-validation of every incoming path.
@@ -501,7 +406,6 @@ typedef struct Config {
* default; only relaxes validation when explicitly requested. */ * default; only relaxes validation when explicitly requested. */
bool trust_sender; bool trust_sender;
// Phase 6: --stop-after / --stop-at
/* Client-only sender-side transfer stop deadlines. --stop-after=MINS stops /* Client-only sender-side transfer stop deadlines. --stop-after=MINS stops
* the transfer after a number of elapsed minutes (checked against * the transfer after a number of elapsed minutes (checked against
* CLOCK_MONOTONIC so clock changes do not skew it); --stop-at=TIME stops at * CLOCK_MONOTONIC so clock changes do not skew it); --stop-at=TIME stops at
@@ -513,7 +417,6 @@ typedef struct Config {
time_t stop_at; /* --stop-at=... absolute wall-clock deadline */ time_t stop_at; /* --stop-at=... absolute wall-clock deadline */
bool stop_at_set; /* true when --stop-at was given */ bool stop_at_set; /* true when --stop-at was given */
// Phase 6: --write-batch / --only-write-batch / --read-batch
/* Client-only residual-batch paths. A residual batch is a self-contained /* Client-only residual-batch paths. A residual batch is a self-contained
* single-file record of the whole source tree (full file images using the * single-file record of the whole source tree (full file images using the
* chunk codec), independent of any live server. --write-batch=FILE runs the * chunk codec), independent of any live server. --write-batch=FILE runs the
@@ -525,6 +428,196 @@ typedef struct Config {
char* write_batch; /* --write-batch=FILE path, or NULL */ char* write_batch; /* --write-batch=FILE path, or NULL */
char* only_write_batch; /* --only-write-batch=FILE path, or NULL */ char* only_write_batch; /* --only-write-batch=FILE path, or NULL */
char* read_batch; /* --read-batch=FILE path, or NULL */ char* read_batch; /* --read-batch=FILE path, or NULL */
/* ===================================================================
* Serialized wire fields. Their members, defaults and send/receive
* sequence are generated from the CONFIG_WIRE_*_FIELDS table above (the
* single source of truth); they are declared here in exact wire order.
* The per-field notes were moved here from their original positions and
* are listed in wire order.
* =================================================================== */
/* copy_links */
// Issue #120: Symlink handling
/* preserve_hard_links */
// Issue #121: Extended metadata preservation
/* preserve_specials */
/* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device
* nodes on the destination by recreating them (mknod/mkfifo) instead of
* transferring content. preserve_specials mirrors rsync --specials (the
* special-file half of -D); preserve_devices mirrors --devices (the device
* half of -D); both CROSS the wire so the receiver knows a special/device
* entry must be recreated rather than written as a regular file. */
/* copy_devices */
/* --copy-devices: copy the CONTENT of a source device as an ordinary regular
* file on the destination (rsync's non-privileged safe mode), instead of
* recreating the device node. CROSSES the wire (receiver treats the entry as
* a regular file, which is the default, so this is belt-and-braces). */
/* write_devices */
/* --write-devices: write the received data directly INTO an existing device
* node on the destination instead of creating a regular file. Dangeroud;
* see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */
/* existing */
// Issue #127: Transfer modes
/* delete_excluded */
/* --delete-excluded: also delete destination entries that were excluded on
* the source. Default (off) matches rsync: excluded paths are protected from
* deletion. Crosses the wire (the sender encodes the choice by whether it
* transmits a protected-prefix list with the keep-set manifest). */
/* force_delete */
/* --force (receiver-side): a regular file may replace a destination
* directory by removing that (possibly non-empty, symlink-safe) directory
* tree first, instead of failing the write. Crosses the wire. */
/* delete_missing_args */
/* --delete-missing-args: implies --ignore-missing-args; additionally each
* missing entry's destination mirror (computed like a present entry's wire
* path) is deleted receiver-side. Crosses the wire and is gated by the
* server's --allow-delete policy like --delete. rsync-parity: independent
* of ordinary --delete processing (it does not imply --delete); a non-empty
* directory mirror is only removed with --force or --delete in effect, and
* the missing-args deletions are not counted toward --max-delete. */
/* preallocate */
/* --preallocate: allocates the destination file's full expected space up
* front (before any data is written) so a transfer that would overflow disk
* fails fast at allocation time and the file is laid out contiguously,
* avoiding fragmentation. Receiver-side, crosses the wire. */
/* max_delete */
/* --max-delete=NUM: the receiver refuses to delete more than NUM entries per
* run (all-or-nothing: when the extras would exceed NUM nothing is removed and
* the transfer fails with a distinct error). -1 == no client limit (the
* server hard bound MAX_SERVER_DELETE_COUNT still applies). */
/* relative */
/* -R/--relative: crosses the wire; with --files-from listed entries keep
* their bare relative destination path (no source-root mirror prefix). */
/* mkpath */
/* --mkpath: crosses the wire. Tells the server to create the destination
* root directory (and missing leading components below its authorized root)
* at connection start instead of requiring it to already exist. */
/* delete_during */
/* rsync deletion-timing family (real from Phase 3). At most one of
delete_before / delete_during / delete_delay / delete_after may be set, and
only together with use_delete (the CLI implies --delete for each of them).
delete_before and delete_during select the EARLY engine mode: the keep-set
manifest is transmitted before any file data and extras are removed then,
acknowledged, before the first data byte. delete_delay and delete_after
select the LATE commit mode: extras are removed only after the whole
transfer has succeeded (plain --delete keeps this mode). The exact
semantics and the divergences from rsync are documented in RSYNC_COMPAT.md
and in config_delete_timing_early() below. */
/* partial_dir */
// PR #174: Partial transfer resumption
/* suffix */
// PR #178: Backup versioning
/* delete_before */
// PR #179: Delete policies
/* checksum */
// PR #183: Checksum comparison
/* compress_choice */
// PR #184: Compression algorithm negotiation
/* basis_dirs */
/* Alternate basis directories, ordered by command-line appearance. Each
* entry's type selects compare/copy/link behavior on an exact match. These
* cross the wire so the receiver can consult them; they are interpreted
* relative to the destination root and confined there. */
/* fuzzy */
/* -y/--fuzzy: when a file must be transferred and the destination holds no
* usable file at the exact path, the receiver may reuse a SIMILAR-named
* existing regular file in the same destination directory as the delta
* basis so the sender transmits only the differences. Crosses the wire
* (the receiver performs the candidate search); the CLI implies
* --incremental + --delta because the similar-basis only matters on the
* receiver-driven delta path. Off by default. */
/* checksum_algo / checksum_seed */
/* --checksum-choice / --cc and --checksum-seed. checksum_algo is the id of
* the whole-file content-digest algorithm used by the per-file --incremental
* handshake (sender computes it, receiver compares it to skip unchanged
* files) and by the basis-dir content verification. checksum_seed is passed
* to xxHash64 (and to the delta block strong hash, low 32 bits); md5 has no
* seed so it is ignored there. Both cross the wire: the receiver MUST hash
* the on-disk old file with the same algorithm and seed to reach a matching
* digest. */
/* munge_links / keep_dirlinks */
/* Phase 4 symlink-trust: both cross the wire (the receiver unmunges symlink
* targets and, with -K, follows an in-root destination symlink-to-directory);
* -k/--copy-dirlinks is sender-only and is never serialized. */
/* numeric_ids */
/* --numeric-ids: no name lookup, use the transmitted numeric ids raw. */
/* chown_uid_set */
/* --chown USER (owner) override; IDENTITY_CURRENT = the receiver's euid. */
/* chown_gid_set */
/* --chown :GROUP (group) override; IDENTITY_CURRENT = the receiver's egid. */
/* usermap */
/* --usermap / --groupmap entries, in order (first match wins). */
/* preserve_atimes */
/* -U/--atimes: preserve source access times on the destination. */
/* preserve_crtimes */
/* -N/--crtimes: capture+transmit source birth time; see RSYNC_COMPAT for the
* receiver not-applied divergence. */
/* omit_dir_times */
/* -O/--omit-dir-times: do not apply mtimes to directories. */
/* omit_link_times */
/* -J/--omit-link-times: do not apply times to symlinks. */
/* fake_super */
/* --fake-super: receiver-only. When set, each written file additionally gets
* a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime
* so a later privileged restore could re-apply them. Crosses the wire. */
/* module */
/* Daemon module selection (Wave A, protocol 2.15.0). Client-composed from a
* host::module/path destination; NULL or "" means "no module" (the ordinary
* standalone-server path). Crosses the wire as a trailing config-frame
* string so the daemon can look the module up in its own config and confine
* the connection to the module's root (never a client-chosen root). */
/* auth_user */
/* Daemon password authentication (A7 remediation, protocol 2.19.0).
* Client-composed from a --password-file whose first meaningful line is
* `user:password`: the client sends ONLY the username in the config frame
* (auth_user); the literal password is kept in auth_password CLIENT-SIDE for
* the duration of the SCRAM challenge/response and is NEVER serialized. Both
* are NULL when the client has no credentials to present; a module WITHOUT
* `auth users` stays open and the server ignores any credentials that do
* arrive (the client sends them opportunistically and the server decides). */
/* iconv_spec */
/* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the
* charset of FILE NAMES at the wire boundary. CONVERT_SPEC is
* "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is
* the remote side's charset and defaults to LOCAL. The sender converts
* every path LOCAL->REMOTE before transmitting it; the receiver converts
* every received path back REMOTE->LOCAL before creating/writing it. The
* FULL SPEC crosses the wire as a trailing config-frame string so each end
* derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL
* (or "") means no conversion: identity with zero overhead. See charset.c
* and the PROTOCOL_VERSION note below. */
/* super_mode */
/* --super / --no-super (P7 Wave E, protocol 2.18.0): receiver-side privilege
* policy for super-user activities confined below the authorized receive
* root. SUPER_MODE_AUTO (default) preserves the pre-existing best-effort
* behavior: the confined super-user operation is ALWAYS attempted and an
* unprivileged attempt is refused by the kernel and skipped per entry.
* SUPER_MODE_ON (--super) explicitly REQUESTS those activities (char/block
* device-node creation, --write-devices); it does NOT imply --numeric-ids and
* never enables ownership application on its own. SUPER_MODE_OFF
* (--no-super) FORBIDS them even when running as root. FastSync NEVER
* elevates privileges (no setuid/seteuid/setgid) and never bypasses the
* fd-relative confinement (file_open_secure_parent, O_NOFOLLOW, root checks);
* --super only permits an attempt that is already confined. Crosses the wire
* as a trailing int so the receiver can enforce the policy. See
* privilege_super_permitted() and identity_ownership_requested() in
* identity.h. */
/* copy_as_set */
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset
* implementation, a documented divergence from rsync's real identity switch:
* the receiver does NOT change its process credentials (FastSync's receiver
* is multithreaded, so a setuid/seteuid drop would be unsafe). Instead the
* receiver FORCES the ownership of every entry it writes to copy_as_uid /
* copy_as_gid through the existing confined, fd-relative identity path
* (fchown/fchownat), which REQUIRES receiver privilege (root); an
* unprivileged receiver REFUSES the whole transfer up front at the config
* handshake (never a silent wrong-ownership result). All three fields CROSS
* the wire as a trailing config-frame block so the receiver learns the
* requested ids; see the PROTOCOL_VERSION note below. */
#define CONFIG_STRUCT_MEMBER(name, ctype, def, kind) ctype name;
CONFIG_WIRE_FIELDS(CONFIG_STRUCT_MEMBER)
#undef CONFIG_STRUCT_MEMBER
} Config; } Config;
/* Phase 5 (remote-option wave): 2.13.0 -> 2.14.0. /* Phase 5 (remote-option wave): 2.13.0 -> 2.14.0.
@@ -699,6 +792,10 @@ void config_delete(Config* config);
void config_burn_auth(Config* config); void config_burn_auth(Config* config);
bool config_send(int file_descriptor, const Config* config); bool config_send(int file_descriptor, const Config* config);
/* Emit the config frame BODY (every serialized field, in wire order) without
* the trailing STATUS_OK handshake. config_send() is this plus the handshake;
* the wire-compatibility golden test uses it to hash the exact byte stream. */
bool config_send_wire_block(int file_descriptor, const Config* config);
Config* config_receive(int file_descriptor); Config* config_receive(int file_descriptor);
bool config_is_remote_dest(const char* s); bool config_is_remote_dest(const char* s);
void config_parse_ssh_dest(Config* config); void config_parse_ssh_dest(Config* config);