diff --git a/src/shared/config.h b/src/shared/config.h index 7061b57..eccede1 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -129,7 +129,7 @@ typedef struct Config { char* compress_choice; } Config; -#define PROTOCOL_VERSION "2.2.0" +#define PROTOCOL_VERSION "2.3.0" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) Config* config_create(void); diff --git a/src/shared/file.c b/src/shared/file.c index 9514e87..5884491 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -172,8 +172,17 @@ bool file_set_authorized_root(int fd, const char* canonical_path) { } bool file_path_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; struct stat st; - return file_stat_secure(path, &st); + bool exists = fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0; + close(parent_fd); + free(leaf); + return exists; } bool file_stat_secure(const char* path, struct stat* st) { @@ -302,8 +311,9 @@ bool file_rename_secure(const char* old_path, const char* new_path) { return ok; } -bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, - bool inplace, bool sparse, const FileMetadata* metadata) { +static bool file_to_disk_secure_impl(const char* path, const void* data, + unsigned long long data_size, bool inplace, bool sparse, + const FileMetadata* metadata, bool no_replace) { char* leaf = NULL; int dirfd = file_open_secure_parent(path, &leaf, true); if (dirfd < 0) @@ -334,8 +344,20 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long if (close(fd) != 0) ok = false; fd = -1; - if (ok && renameat(dirfd, tmp, dirfd, leaf) != 0) - ok = false; + if (ok) { + if (no_replace) { + /* The probe and commit cannot be one operation. A concurrent + creator may win; EEXIST is then the requested skip. */ + if (linkat(dirfd, tmp, dirfd, leaf, 0) == 0 || errno == EEXIST) { + if (unlinkat(dirfd, tmp, 0) != 0 && errno != ENOENT) + ok = false; + } else { + ok = false; + } + } else if (renameat(dirfd, tmp, dirfd, leaf) != 0) { + ok = false; + } + } if (!ok) unlinkat(dirfd, tmp, 0); } @@ -347,6 +369,17 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long return ok; } +bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse, const FileMetadata* metadata) { + return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, metadata, false); +} + +bool file_to_disk_secure_no_replace(const char* path, const void* data, + unsigned long long data_size, bool sparse, + const FileMetadata* metadata) { + return file_to_disk_secure_impl(path, data, data_size, false, sparse, metadata, true); +} + bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse) { if (!path || (!data && data_size != 0) || has_path_traversal(path)) diff --git a/src/shared/file.h b/src/shared/file.h index 23dac26..d6ff3d4 100644 --- a/src/shared/file.h +++ b/src/shared/file.h @@ -32,5 +32,8 @@ bool file_ensure_directory_secure(const char* path); bool file_rename_secure(const char* old_path, const char* new_path); bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse, const FileMetadata* metadata); +bool file_to_disk_secure_no_replace(const char* path, const void* data, + unsigned long long data_size, bool sparse, + const FileMetadata* metadata); #endif diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index 6ef8a8d..a371ff3 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -118,8 +118,11 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi } } - bool ok = file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace, sparse, - file->metadata); + bool ok = config && config->ignore_existing + ? file_to_disk_secure_no_replace(disk_path, file->data->data, file->data->size, + sparse, file->metadata) + : file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace, + sparse, file->metadata); free(parent_copy); free(backup_path); free(confined_backup); diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index ec974d3..1d96678 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -225,16 +225,20 @@ class TestIgnoreExisting: with open(existing_file, "wb") as f: f.write(b"destination content\n") new_source = os.path.join(SOURCE_DIR, "new.txt") - with open(new_source, "wb") as f: - f.write(b"new file\n") + try: + with open(new_source, "wb") as f: + f.write(b"new file\n") - result, _ = run_client(SOURCE_DIR, DEST_DIR, - flags=["--ignore-existing"], port=shared_server.port) - assert result.returncode == 0, f"Sync failed: {(result.stderr or result.stdout)[:200]}" - with open(existing_file, "rb") as f: - assert f.read() == b"destination content\n" - with open(os.path.join(received, "new.txt"), "rb") as f: - assert f.read() == b"new file\n" + result, _ = run_client(SOURCE_DIR, DEST_DIR, + flags=["--ignore-existing"], port=shared_server.port) + assert result.returncode == 0, f"Sync failed: {(result.stderr or result.stdout)[:200]}" + with open(existing_file, "rb") as f: + assert f.read() == b"destination content\n" + with open(os.path.join(received, "new.txt"), "rb") as f: + assert f.read() == b"new file\n" + finally: + if os.path.lexists(new_source): + os.unlink(new_source) class TestDelete: diff --git a/tests/test_config.c b/tests/test_config.c index ea62295..2f68d66 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -188,11 +188,11 @@ static void test_config_send_receive() { } static void test_config_send_receive_version_mismatch() { - /* Create a config with a different protocol version */ + /* A peer using the previous wire format must be rejected. */ Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); free(cfg->version); - cfg->version = str_dup("0.0"); + cfg->version = str_dup("2.2.0"); cfg->send_directory = str_dup("/src"); cfg->receive_root_directory = str_dup("/dst");