feat: basis-dir config/CLI for --compare-dest/--copy-dest/--link-dest
Replace the vestigial single compare_dest/copy_dest/link_dest Config fields with an ordered BasisDest list (type + path per entry) that is serialized to the receiver and interpreted relative to the destination root. Paths must be relative with no '.'/'..' components (confined like --backup-dir); trailing slashes are normalized. Wire layout changes, so PROTOCOL_VERSION -> 2.8.0. CLI: each flag is parsed in both --flag=DIR and --flag DIR forms, is repeatable, and keeps command-line order as basis priority. Supplying any basis dir implies --incremental (and therefore metadata) on the sender because the unchanged decision is receiver-side; combining basis dirs with -s chunk serialization is rejected in validate_config. Usage text updated.
This commit is contained in:
@@ -114,6 +114,27 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Validate and append one --compare-dest/--copy-dest/--link-dest directory.
|
||||
* The path is interpreted on the receiver relative to the destination root,
|
||||
* so it must be a non-empty relative path with no "." / ".." components (an
|
||||
* absolute or escaping path is rejected up front instead of failing on the
|
||||
* server). Returns 0 on success, -1 on error. */
|
||||
static int set_basis_dest_option(Config* config, BasisDestType type, const char* value,
|
||||
const char* option_name) {
|
||||
if (!value || !value[0]) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", option_name);
|
||||
return -1;
|
||||
}
|
||||
if (config_basis_append(config, type, value) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s requires a non-empty relative directory name with no '.', '..', or absolute "
|
||||
"path (resolved below the destination root)",
|
||||
option_name);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int set_stderr_mode(const char* value) {
|
||||
if (strcmp(value, "errors") == 0 || strcmp(value, "e") == 0)
|
||||
log_set_stderr_mode(LOG_STDERR_ERRORS);
|
||||
@@ -974,6 +995,36 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
}
|
||||
log_message(LOG_LEVEL_ERROR, "%s is not supported yet (xxHash64 is used)", argv[i]);
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--compare-dest=", 15) == 0) {
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COMPARE, argv[i] + 15, "--compare-dest") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--compare-dest", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COMPARE, argv[++i], "--compare-dest") != 0)
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--copy-dest=", 12) == 0) {
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COPY, argv[i] + 12, "--copy-dest") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--copy-dest", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COPY, argv[++i], "--copy-dest") != 0)
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--link-dest=", 12) == 0) {
|
||||
if (set_basis_dest_option(config, BASIS_DEST_LINK, argv[i] + 12, "--link-dest") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--link-dest", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_basis_dest_option(config, BASIS_DEST_LINK, argv[++i], "--link-dest") != 0)
|
||||
return -1;
|
||||
} else if (argv[i][0] == '-') {
|
||||
char* escaped = output_escape(argv[i], false);
|
||||
fprintf(stderr, "Unknown option: %s\n", escaped ? escaped : "<allocation failed>");
|
||||
@@ -1010,6 +1061,13 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
config->files_from_set = set;
|
||||
}
|
||||
|
||||
/* The "unchanged" decision for --compare-dest/--copy-dest/--link-dest must
|
||||
* be made on the receiver against the basis directories, which requires the
|
||||
* per-file STATUS_CHECK handshake: basis-dir options therefore imply
|
||||
* --incremental (and, via the block below, metadata) on the sender. */
|
||||
if (config_has_basis(config))
|
||||
config->use_incremental = true;
|
||||
|
||||
/* Incremental and delta transfers need metadata unless the user disabled it. */
|
||||
if ((config->use_incremental || config->use_delta) && !config->use_metadata &&
|
||||
!config->metadata_explicitly_disabled) {
|
||||
|
||||
@@ -11,6 +11,12 @@ bool validate_config(const Config* config) {
|
||||
print_usage();
|
||||
return false;
|
||||
}
|
||||
if (config_has_basis(config) && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--compare-dest/--copy-dest/--link-dest require per-file incremental checks and "
|
||||
"cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
|
||||
log_message(LOG_LEVEL_ERROR, "-f/--sendfile cannot be combined with -c (compression) or -s "
|
||||
"(chunk serialization)");
|
||||
|
||||
@@ -58,6 +58,13 @@ void print_usage(void) {
|
||||
printf(" -@, --modify-window <sec> Modification time tolerance\n");
|
||||
printf(" -u, --update Skip files newer than the source on receiver\n");
|
||||
printf(" --existing Skip files not already present at destination\n");
|
||||
printf(" --compare-dest <dir> Treat DIR (relative to destination root) as an extra\n");
|
||||
printf(" comparison basis: unchanged files are not transferred\n");
|
||||
printf(" (requires --incremental, which is implied)\n");
|
||||
printf(" --copy-dest <dir> Like --compare-dest, but copies the unchanged file from DIR\n");
|
||||
printf(" into the destination instead of transferring its data\n");
|
||||
printf(" --link-dest <dir> Like --copy-dest, but hard-links the unchanged file from DIR\n");
|
||||
printf(" into the destination (repeatable; earlier DIRs win)\n");
|
||||
printf(" --checksum-choice, --cc <alg> Checksum algorithm (not supported yet; xxHash64 is "
|
||||
"used)\n");
|
||||
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
|
||||
|
||||
+103
-8
@@ -109,9 +109,8 @@ static void config_set_defaults(Config* config) {
|
||||
config->rsync_path = NULL;
|
||||
config->old_args = false;
|
||||
config->temp_dir = NULL;
|
||||
config->compare_dest = NULL;
|
||||
config->copy_dest = NULL;
|
||||
config->link_dest = NULL;
|
||||
config->basis_dirs = NULL;
|
||||
config->basis_count = 0;
|
||||
config->partial_dir = NULL;
|
||||
config->suffix = NULL;
|
||||
config->delete_before = false;
|
||||
@@ -188,6 +187,63 @@ Config* config_create(void) {
|
||||
return config;
|
||||
}
|
||||
|
||||
bool config_has_basis(const Config* config) {
|
||||
return config && config->basis_count > 0;
|
||||
}
|
||||
|
||||
/* A basis-dir path travels from the client to the receiver and is resolved
|
||||
* below the destination root, so it must be a non-empty relative path with no
|
||||
* "." or ".." component and no traversal: an absolute or escaping path would
|
||||
* make the receiver read or link files outside its authorized root. */
|
||||
bool config_basis_path_valid(const char* path) {
|
||||
if (!path || path[0] == '\0' || path[0] == '/' || has_path_traversal(path))
|
||||
return false;
|
||||
if (strcmp(path, ".") == 0)
|
||||
return false;
|
||||
char* dup = str_dup(path);
|
||||
if (!dup)
|
||||
return false;
|
||||
bool ok = true;
|
||||
char* saveptr = NULL;
|
||||
for (char* part = strtok_r(dup, "/", &saveptr); part; part = strtok_r(NULL, "/", &saveptr)) {
|
||||
if (strcmp(part, ".") == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (strcmp(part, "..") == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(dup);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int config_basis_append(Config* config, BasisDestType type, const char* path) {
|
||||
if (!config || !config_basis_path_valid(path) ||
|
||||
(type != BASIS_DEST_COMPARE && type != BASIS_DEST_COPY && type != BASIS_DEST_LINK) ||
|
||||
config->basis_count >= MAX_BASIS_DIRS)
|
||||
return -1;
|
||||
BasisDest* grown = realloc(config->basis_dirs, (config->basis_count + 1) * sizeof(BasisDest));
|
||||
if (!grown)
|
||||
return -1;
|
||||
config->basis_dirs = grown;
|
||||
/* Normalize a user-supplied trailing slash away so the stored path matches
|
||||
the delete-walker prefix form exactly. */
|
||||
size_t len = strlen(path);
|
||||
while (len > 1 && path[len - 1] == '/')
|
||||
len--;
|
||||
char* dup = malloc(len + 1);
|
||||
if (!dup)
|
||||
return -1;
|
||||
memcpy(dup, path, len);
|
||||
dup[len] = '\0';
|
||||
config->basis_dirs[config->basis_count].type = type;
|
||||
config->basis_dirs[config->basis_count].path = dup;
|
||||
config->basis_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool config_is_remote_dest(const char* s) {
|
||||
if (s == NULL)
|
||||
return false;
|
||||
@@ -244,9 +300,13 @@ void config_delete(Config* config) {
|
||||
free(config->rsh_command);
|
||||
free(config->rsync_path);
|
||||
free(config->temp_dir);
|
||||
free(config->compare_dest);
|
||||
free(config->copy_dest);
|
||||
free(config->link_dest);
|
||||
for (int i = 0; i < config->basis_count; i++) {
|
||||
free(config->basis_dirs[i].path);
|
||||
config->basis_dirs[i].path = NULL;
|
||||
}
|
||||
free(config->basis_dirs);
|
||||
config->basis_dirs = NULL;
|
||||
config->basis_count = 0;
|
||||
free(config->partial_dir);
|
||||
free(config->suffix);
|
||||
free(config->address);
|
||||
@@ -333,6 +393,17 @@ static bool send_resume_options(int fd, const Config* c) {
|
||||
send_str(fd, c->chmod_spec ? c->chmod_spec : "") && send_skip_compress_options(fd, c);
|
||||
}
|
||||
|
||||
static bool send_basis_options(int fd, const Config* c) {
|
||||
if (!send_int(fd, c->basis_count))
|
||||
return false;
|
||||
for (int i = 0; i < c->basis_count; i++) {
|
||||
if (!send_int(fd, (int)c->basis_dirs[i].type) ||
|
||||
!send_str(fd, c->basis_dirs[i].path ? c->basis_dirs[i].path : ""))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool receive_core_fields(int fd, Config* c) {
|
||||
int value;
|
||||
if (!receive_wire_bool(fd, &c->eight_bit_output))
|
||||
@@ -479,12 +550,35 @@ static bool receive_resume_options(int fd, Config* c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool receive_basis_options(int fd, Config* c) {
|
||||
int count;
|
||||
if (!receive_int(fd, &count))
|
||||
return false;
|
||||
if (count < 0 || count > MAX_BASIS_DIRS)
|
||||
return false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int type;
|
||||
if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK)
|
||||
return false;
|
||||
char* path = receive_str(fd);
|
||||
if (!path)
|
||||
return false;
|
||||
bool ok = config_basis_path_valid(path);
|
||||
if (ok)
|
||||
ok = config_basis_append(c, (BasisDestType)type, path) == 0;
|
||||
free(path);
|
||||
if (!ok)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool config_send(int file_descriptor, const Config* config) {
|
||||
protocol_session_set_max_alloc(NULL, config->max_alloc);
|
||||
if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) ||
|
||||
!send_file_options(file_descriptor, config) ||
|
||||
!send_selection_options(file_descriptor, config) ||
|
||||
!send_resume_options(file_descriptor, config))
|
||||
!send_resume_options(file_descriptor, config) || !send_basis_options(file_descriptor, config))
|
||||
return false;
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
@@ -516,7 +610,8 @@ Config* config_receive(int file_descriptor) {
|
||||
!receive_delta_fields(file_descriptor, config) ||
|
||||
!receive_file_options(file_descriptor, config) ||
|
||||
!receive_selection_options(file_descriptor, config) ||
|
||||
!receive_resume_options(file_descriptor, config))
|
||||
!receive_resume_options(file_descriptor, config) ||
|
||||
!receive_basis_options(file_descriptor, config))
|
||||
goto error;
|
||||
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||
strcmp(config->compress_choice, "none") != 0) {
|
||||
|
||||
+32
-4
@@ -12,6 +12,22 @@ typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
|
||||
Config can carry it; the concrete type lives in delay_updates.h. */
|
||||
typedef struct DelayUpdatesContext DelayUpdatesContext;
|
||||
|
||||
/* Alternate basis-directory modes (--compare-dest / --copy-dest /
|
||||
* --link-dest). Each flag adds one entry to the ordered Config->basis_dirs
|
||||
* list; the receiver consults entries in command-line order and stops at the
|
||||
* first exact match, mirroring rsync's basis-dir priority rules. */
|
||||
typedef enum {
|
||||
BASIS_DEST_NONE = 0,
|
||||
BASIS_DEST_COMPARE, /* compare only: never copies, never materializes */
|
||||
BASIS_DEST_COPY, /* local copy of the matched basis file */
|
||||
BASIS_DEST_LINK /* hard link to the matched basis file */
|
||||
} BasisDestType;
|
||||
|
||||
typedef struct BasisDest {
|
||||
BasisDestType type;
|
||||
char* path; /* relative to the destination root (receiver-confined) */
|
||||
} BasisDest;
|
||||
|
||||
typedef struct Config {
|
||||
char* version;
|
||||
char* send_directory;
|
||||
@@ -134,9 +150,12 @@ typedef struct Config {
|
||||
char* rsync_path;
|
||||
bool old_args;
|
||||
char* temp_dir;
|
||||
char* compare_dest;
|
||||
char* copy_dest;
|
||||
char* link_dest;
|
||||
/* 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;
|
||||
@@ -174,8 +193,10 @@ typedef struct Config {
|
||||
DelayUpdatesContext* delay_context;
|
||||
} Config;
|
||||
|
||||
#define PROTOCOL_VERSION "2.7.0"
|
||||
#define PROTOCOL_VERSION "2.8.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
|
||||
Config* config_create(void);
|
||||
void config_delete(Config* config);
|
||||
@@ -184,4 +205,11 @@ Config* config_receive(int file_descriptor);
|
||||
bool config_is_remote_dest(const char* s);
|
||||
void config_parse_ssh_dest(Config* config);
|
||||
|
||||
/* True when at least one --compare-dest/--copy-dest/--link-dest was set. */
|
||||
bool config_has_basis(const Config* config);
|
||||
/* Append one basis-dir entry. Returns 0 on success, -1 on allocation failure. */
|
||||
int config_basis_append(Config* config, BasisDestType type, const char* path);
|
||||
/* Validate a client-provided basis-dir path (relative, confined, non-empty). */
|
||||
bool config_basis_path_valid(const char* path);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user