feat: implement -R/--relative, --no-implied-dirs, --dirs/-d, --mkpath
CI / lint (pull_request) Successful in 26s
CI / sanitizers (address) (pull_request) Failing after 41s
CI / sanitizers (undefined) (pull_request) Successful in 40s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Failing after 37s
CI / build-and-test (pull_request) Failing after 2m35s
CI / lint (pull_request) Successful in 26s
CI / sanitizers (address) (pull_request) Failing after 41s
CI / sanitizers (undefined) (pull_request) Successful in 40s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Failing after 37s
CI / build-and-test (pull_request) Failing after 2m35s
RSYNC_COMPAT Phase-2 row M: path-list construction and destination directory creation while preserving traversal safety. - -R/--relative with --files-from: transmit each listed entry under its bare relative destination path (no source-root mirror). Files keep an absolute local read path plus a separate wire/dest path (File.send_path); manifest, incremental quick-check and change output follow the wire path, so --delete and --remove-source-files stay consistent. -R without --files-from is unchanged (full mirror). - --no-implied-dirs: client-only, only meaningful with -R + --files-from. A listed file whose parent dir is not itself (or via an ancestor) explicitly listed cannot be placed; the run fails up front with a clear error. No effect otherwise. - --dirs/-d + --old-dirs/--old-d aliases: -d <dir> transmits the source root as an explicit empty directory entry (STATUS_MKDIR frame); with --files-from listed dirs are created empty and listed files transferred, never descending. Works single-threaded, -m (sequential scanner in the -m scan thread) and chunk-serialization (per-file type marker). Directory entries appear in the delete manifest. - --mkpath: new wire bool; server creates the destination root (and missing leading components under its authorized root) at connection start. A missing destination root is now rejected by default. - Protocol bumped to 2.7.0 (mkpath wire field + STATUS_MKDIR + chunk type marker). All receive paths funnel through file_save_to_disk_full which creates directories via the secure confined mkdir engine; dir entries are excluded from --remove-source-files outcome acknowledgements on both ends. - Unit coverage: CLI parse (relative/dirs aliases/mkpath/no-implied-dirs), config round-trip (relative + mkpath), scanner --dirs non-recursion and -R send_path (sequential + parallel), receiver dir-entry save. - Integration coverage: TestRelativeFilesFrom, TestNoImpliedDirs, TestDirs, TestMkpath (single and -m). - RSYNC_COMPAT: 4 rows move to Implemented (Summary 67/3/5/1/71 = 147).
This commit is contained in:
+34
-5
@@ -65,7 +65,7 @@ void chunk_destroy(void* item) {
|
||||
|
||||
static unsigned long long per_file_serialize_size(File* file, bool use_metadata) {
|
||||
unsigned long long size = sizeof(size_t);
|
||||
size_t path_len = strlen(file->path);
|
||||
size_t path_len = strlen(file_wire_path(file));
|
||||
unsigned long long metadata_size =
|
||||
use_metadata ? sizeof(int) + (file->metadata ? FILE_METADATA_WIRE_SIZE : 0) : 0;
|
||||
if ((unsigned long long)path_len > ULLONG_MAX - size)
|
||||
@@ -74,6 +74,10 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
|
||||
if (metadata_size > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += metadata_size;
|
||||
/* Entry type marker: 0 = regular file, 1 = explicit directory entry. */
|
||||
if (sizeof(int) > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += sizeof(int);
|
||||
if (sizeof(size_t) > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += sizeof(size_t);
|
||||
@@ -89,7 +93,8 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (!chunk->items[i] || !chunk->items[i]->path || !chunk->items[i]->data ||
|
||||
(chunk->items[i]->data->size > 0 && !chunk->items[i]->data->data) ||
|
||||
chunk->items[i]->path[0] == '\0' || has_path_traversal(chunk->items[i]->path))
|
||||
chunk->items[i]->path[0] == '\0' || has_path_traversal(chunk->items[i]->path) ||
|
||||
(file_wire_path(chunk->items[i]))[0] == '\0')
|
||||
return NULL;
|
||||
unsigned long long file_size = per_file_serialize_size(chunk->items[i], use_metadata);
|
||||
if (file_size == 0 || file_size > ULLONG_MAX - data_size || data_size + file_size > SIZE_MAX)
|
||||
@@ -104,19 +109,25 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
|
||||
char* data_pointer = data->data;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
File* file = chunk->items[i];
|
||||
size_t path_len = strlen(file->path);
|
||||
const char* wire_path = file_wire_path(file);
|
||||
size_t path_len = strlen(wire_path);
|
||||
memcpy(data_pointer, &path_len, sizeof(size_t));
|
||||
data_pointer += sizeof(size_t);
|
||||
memcpy(data_pointer, file->path, path_len);
|
||||
memcpy(data_pointer, wire_path, path_len);
|
||||
data_pointer += path_len;
|
||||
|
||||
int entry_type = file->is_dir ? 1 : 0;
|
||||
memcpy(data_pointer, &entry_type, sizeof(int));
|
||||
data_pointer += sizeof(int);
|
||||
|
||||
if (use_metadata)
|
||||
metadata_to_buf(&data_pointer, file->metadata);
|
||||
|
||||
size_t file_data_size = file->data->size;
|
||||
memcpy(data_pointer, &file_data_size, sizeof(size_t));
|
||||
data_pointer += sizeof(size_t);
|
||||
memcpy(data_pointer, file->data->data, file_data_size);
|
||||
if (file_data_size > 0)
|
||||
memcpy(data_pointer, file->data->data, file_data_size);
|
||||
data_pointer += file_data_size;
|
||||
}
|
||||
return data;
|
||||
@@ -187,6 +198,24 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (remaining_size < sizeof(int)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for entry type");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
int entry_type;
|
||||
memcpy(&entry_type, data_pointer, sizeof(int));
|
||||
if (entry_type != 0 && entry_type != 1) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
file->is_dir = entry_type == 1;
|
||||
data_pointer += sizeof(int);
|
||||
remaining_size -= sizeof(int);
|
||||
|
||||
if (use_metadata) {
|
||||
if (remaining_size < sizeof(int)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
|
||||
|
||||
+8
-2
@@ -102,6 +102,9 @@ static void config_set_defaults(Config* config) {
|
||||
config->prune_empty_dirs = false;
|
||||
config->one_file_system = false;
|
||||
config->relative = false;
|
||||
config->no_implied_dirs = false;
|
||||
config->dirs = false;
|
||||
config->mkpath = false;
|
||||
config->rsh_command = NULL;
|
||||
config->rsync_path = NULL;
|
||||
config->old_args = false;
|
||||
@@ -159,7 +162,8 @@ static bool validate_received_config(const Config* config) {
|
||||
valid_wire_bool(config->use_fsync) && valid_wire_bool(config->append_verify) &&
|
||||
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) &&
|
||||
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
||||
valid_wire_bool(config->delay_updates) && !(config->delay_updates && config->inplace) &&
|
||||
valid_wire_bool(config->delay_updates) && valid_wire_bool(config->mkpath) &&
|
||||
!(config->delay_updates && config->inplace) &&
|
||||
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
||||
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
||||
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
||||
@@ -307,7 +311,7 @@ static bool send_selection_options(int fd, const Config* c) {
|
||||
send_int(fd, c->use_fsync) && send_int(fd, c->append_verify) &&
|
||||
send_int(fd, c->delete_excluded) && send_int(fd, c->delete_after) &&
|
||||
send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && send_int(fd, c->relative) &&
|
||||
send_int(fd, c->prune_empty_dirs);
|
||||
send_int(fd, c->prune_empty_dirs) && send_int(fd, c->mkpath);
|
||||
}
|
||||
|
||||
static bool send_skip_compress_options(int fd, const Config* c) {
|
||||
@@ -412,6 +416,8 @@ static bool receive_selection_options(int fd, Config* c) {
|
||||
return false;
|
||||
if (!receive_wire_bool(fd, &c->prune_empty_dirs))
|
||||
return false;
|
||||
if (!receive_wire_bool(fd, &c->mkpath))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -115,7 +115,19 @@ typedef struct Config {
|
||||
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 */
|
||||
/* -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
|
||||
* listed file whose ancestor directory is not itself explicitly listed. */
|
||||
bool no_implied_dirs;
|
||||
/* -d/--dirs: client-only. Transfer the directory entries named by the
|
||||
* source argument / --files-from list without recursing into contents. */
|
||||
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
|
||||
char* rsh_command;
|
||||
@@ -162,7 +174,7 @@ typedef struct Config {
|
||||
DelayUpdatesContext* delay_context;
|
||||
} Config;
|
||||
|
||||
#define PROTOCOL_VERSION "2.6.0"
|
||||
#define PROTOCOL_VERSION "2.7.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
|
||||
Config* config_create(void);
|
||||
|
||||
@@ -73,6 +73,7 @@ File* file_create(const char* path) {
|
||||
|
||||
memcpy(file->path, path, path_len);
|
||||
file->path[path_len] = '\0';
|
||||
file->send_path = NULL;
|
||||
file->data = data_create_reserve(0);
|
||||
if (file->data == NULL) {
|
||||
free(file->path);
|
||||
@@ -81,6 +82,7 @@ File* file_create(const char* path) {
|
||||
}
|
||||
file->metadata = NULL;
|
||||
file->skip = false;
|
||||
file->is_dir = false;
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -94,6 +96,8 @@ void file_destroy(void* item) {
|
||||
file->metadata = NULL;
|
||||
free(file->path);
|
||||
file->path = NULL;
|
||||
free(file->send_path);
|
||||
file->send_path = NULL;
|
||||
free(file);
|
||||
}
|
||||
|
||||
@@ -323,6 +327,27 @@ bool file_ensure_directory_secure(const char* path) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* True when `path` resolves to an existing directory below the authorized root
|
||||
* (never creating anything). Used by the server to decide whether a client's
|
||||
* destination root already exists. */
|
||||
bool file_directory_exists_secure(const char* path) {
|
||||
if (!path)
|
||||
return false;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(path, &leaf, false);
|
||||
if (parent_fd < 0)
|
||||
return false;
|
||||
int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (dir_fd < 0 && errno == ENOENT)
|
||||
dir_fd = -1;
|
||||
bool ok = dir_fd >= 0;
|
||||
if (dir_fd >= 0)
|
||||
close(dir_fd);
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool file_rename_secure(const char* old_path, const char* new_path) {
|
||||
char *old_leaf = NULL, *new_leaf = NULL;
|
||||
int old_parent = file_open_secure_parent(old_path, &old_leaf, false);
|
||||
|
||||
@@ -30,6 +30,7 @@ bool file_stat_secure(const char* path, struct stat* st);
|
||||
bool file_destination_is_newer_secure(const char* path, const FileMetadata* metadata);
|
||||
int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs);
|
||||
bool file_ensure_directory_secure(const char* path);
|
||||
bool file_directory_exists_secure(const char* path);
|
||||
bool file_rename_secure(const char* old_path, const char* new_path);
|
||||
/* Open a private 0700 directory (creating it on demand) that must live below
|
||||
the authorized root. Used for the --temp-dir scratch directory and the
|
||||
|
||||
@@ -119,6 +119,24 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* Explicit directory entries (--dirs) carry an empty payload; the entry is
|
||||
created as a directory under the receive root, applying the same secure
|
||||
mkdir-parent semantics as regular writes. Directories are created
|
||||
immediately (they are never staged by --delay-updates, matching rsync,
|
||||
where directory creation is not delayed). */
|
||||
if (file->is_dir) {
|
||||
if (file->path[0] == '\0' || has_path_traversal(file->path)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid directory path received");
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
char* dir_path = path_cat(root_directory, file->path);
|
||||
if (!dir_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool ok = file_ensure_directory_secure(dir_path);
|
||||
free(dir_path);
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* These options arrive from the client. They are names below the server
|
||||
root, never independent filesystem roots. --temp-dir is confined exactly
|
||||
like --backup-dir/--partial-dir: an absolute or `..`-escaping scratch
|
||||
@@ -749,6 +767,31 @@ File* file_receive(const Config* config, int file_descriptor) {
|
||||
return file;
|
||||
}
|
||||
|
||||
/* Receive an explicit directory entry (--dirs): a STATUS_MKDIR frame carries
|
||||
only the destination path; the entry carries no payload. The same path
|
||||
validation as a regular file applies (non-empty, relative-or-mirrored, no
|
||||
traversal), and the created File is routed through the regular store_file
|
||||
sink so single-threaded and -m receivers handle directories identically. */
|
||||
File* file_receive_directory(int file_descriptor) {
|
||||
char* path = receive_str(file_descriptor);
|
||||
if (path == NULL)
|
||||
return NULL;
|
||||
if (path[0] == '\0' || has_path_traversal(path)) {
|
||||
char* escaped_path = output_escape(path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid received directory path: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
free(path);
|
||||
return NULL;
|
||||
}
|
||||
File* file = file_create(path);
|
||||
free(path);
|
||||
if (file == NULL)
|
||||
return NULL;
|
||||
file->is_dir = true;
|
||||
return file;
|
||||
}
|
||||
|
||||
int receive_manifest(int fd, const Config* config, int* next_status) {
|
||||
if (!config) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
/* Server-side file receive/save path. */
|
||||
|
||||
File* file_receive(const Config* config, int file_descriptor);
|
||||
File* file_receive_directory(int file_descriptor);
|
||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
||||
int receive_manifest(int fd, const Config* config, int* next_status);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ bool file_send_single_calls_with_skip(File* file, int file_descriptor, bool use_
|
||||
}
|
||||
data_to_send = compressed_data;
|
||||
}
|
||||
if (send_path && !send_str(file_descriptor, file->path)) {
|
||||
if (send_path && !send_str(file_descriptor, file_wire_path(file))) {
|
||||
data_destroy(compressed_data);
|
||||
return false;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
|
||||
send_path, skip_suffixes, skip_count,
|
||||
compression_threads);
|
||||
|
||||
if (send_path && !send_str(file_descriptor, file->path))
|
||||
if (send_path && !send_str(file_descriptor, file_wire_path(file)))
|
||||
return false;
|
||||
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
|
||||
return false;
|
||||
|
||||
@@ -17,9 +17,24 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
char* path;
|
||||
/* Sender-side override for the path transmitted on the wire (and used for
|
||||
* the delete manifest / change output). NULL means "use `path`". With
|
||||
* -R + --files-from this holds the entry's bare relative destination path,
|
||||
* while `path` stays the absolute local source path the client reads from.
|
||||
* Never populated on the receiver. */
|
||||
char* send_path;
|
||||
Data* data;
|
||||
FileMetadata* metadata;
|
||||
bool skip;
|
||||
/* True when this entry is an explicit directory entry (--dirs mode): the
|
||||
* receiver creates the directory instead of writing a regular file. */
|
||||
bool is_dir;
|
||||
} File;
|
||||
|
||||
/* The path that should be sent on the wire and used for the receiver-side
|
||||
* destination layout (see send_path). */
|
||||
static inline const char* file_wire_path(const File* file) {
|
||||
return file && file->send_path ? file->send_path : (file ? file->path : NULL);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -295,8 +295,9 @@ int write_thread(void* pipeline_context) {
|
||||
}
|
||||
}
|
||||
/* Record the per-file outcome so a --remove-source-files sender learns
|
||||
which sources were actually written versus skipped on the receiver. */
|
||||
if (context->config->remove_source_files &&
|
||||
which sources were actually written versus skipped on the receiver.
|
||||
Explicit directory entries have no source and are never acknowledged. */
|
||||
if (context->config->remove_source_files && !file->is_dir &&
|
||||
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
file_destroy(file);
|
||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||
|
||||
@@ -67,7 +67,10 @@ enum NET_STATUS {
|
||||
STATUS_DELTA_DATA,
|
||||
STATUS_KEEPALIVE,
|
||||
STATUS_ABORT,
|
||||
STATUS_CHECK_BATCH
|
||||
STATUS_CHECK_BATCH,
|
||||
/* An explicit directory entry (--dirs): the sender transmits only the path;
|
||||
* the receiver creates the directory below the receive root. */
|
||||
STATUS_MKDIR
|
||||
};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd);
|
||||
|
||||
Reference in New Issue
Block a user