diff --git a/README.md b/README.md index fdbd9f3..6962093 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,7 @@ defaults to the current directory. | ## Protocol and Security -FastSync protocol version `2.2.0` is shared by the client and server. The +FastSync protocol version `2.3.0` is shared by the client and server. The current protocol is sender-driven and includes configuration negotiation, incremental checks, checksums, manifests, keep-alives, abort handling, and FastSync-native delta messages. Client and server versions must currently diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index a371ff3..c18272d 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -22,7 +22,9 @@ #define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) { - bool backup_enabled = config && config->backup; + /* Backups are incompatible with ignore-existing: moving the entry first + would make a concurrent no-replace commit overwrite its old name. */ + bool backup_enabled = config && config->backup && !config->ignore_existing; bool inplace = config && config->inplace; bool sparse = config && config->preserve_sparse; const char* backup_suffix = (config && config->suffix) ? config->suffix : "~"; @@ -64,9 +66,8 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi /* --ignore-existing checks the final destination before partial files or overwrite policies can modify it. */ if (config && config->ignore_existing) { - struct stat destination_stat; char* final_path = path_cat(root_directory, file->path); - bool exists = final_path && file_stat_secure(final_path, &destination_stat); + bool exists = final_path && file_path_exists_secure(final_path); free(final_path); if (exists) { free(confined_backup); diff --git a/tests/test_file.c b/tests/test_file.c index cdb5385..aae542b 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -120,6 +120,67 @@ static void test_file_save_to_disk_ignore_existing() { rmdir("test_ignore_existing_tmp"); } +static void test_file_save_to_disk_ignore_existing_entry_types() { + const char* root = "test_ignore_existing_entries_tmp"; + const char* directory = "test_ignore_existing_entries_tmp/directory"; + const char* link = "test_ignore_existing_entries_tmp/link"; + const char* target = "test_ignore_existing_entries_tmp/target"; + const char* backup = "test_ignore_existing_entries_tmp/backup.txt~"; + const char* backup_file = "test_ignore_existing_entries_tmp/backup.txt"; + Config* config = config_create(); + File* file = file_create("unused"); + + unlink(link); + unlink(target); + unlink(backup); + unlink(backup_file); + rmdir(directory); + rmdir(root); + EXPECT_NOT_NULL(config); + EXPECT_NOT_NULL(file); + // cppcheck-suppress knownConditionTrueFalse + if (!config || !file) + return; + config->ignore_existing = true; + config->backup = true; + file->data->data = malloc(3); + EXPECT_NOT_NULL(file->data->data); + // cppcheck-suppress knownConditionTrueFalse + if (!file->data->data) { + file_destroy(file); + config_delete(config); + return; + } + memcpy(file->data->data, "new", 3); + file->data->size = 3; + + EXPECT_EQ_INT(mkdir(root, 0755), 0); + EXPECT_EQ_INT(mkdir(directory, 0755), 0); + EXPECT_TRUE(file_write_to_disk(target, "old", 3, false, false)); + EXPECT_EQ_INT(symlink("target", link), 0); + free(file->path); + file->path = str_dup("directory"); + EXPECT_TRUE(file_save_to_disk(root, file, config)); + free(file->path); + file->path = str_dup("link"); + EXPECT_TRUE(file_save_to_disk(root, file, config)); + + free(file->path); + file->path = str_dup("backup.txt"); + EXPECT_TRUE(file_write_to_disk(backup_file, "old", 3, false, false)); + EXPECT_TRUE(file_save_to_disk(root, file, config)); + EXPECT_TRUE(file_path_exists_secure(backup_file)); + EXPECT_FALSE(file_path_exists_secure(backup)); + + file_destroy(file); + config_delete(config); + unlink(link); + unlink(target); + unlink(backup_file); + rmdir(directory); + rmdir(root); +} + static void test_file_write_to_disk_basic() { const char* content = "Basic file_write_to_disk test"; EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content), @@ -491,6 +552,7 @@ void test_file() { test_file_load_data_missing_file(); test_file_save_to_disk(); test_file_save_to_disk_ignore_existing(); + test_file_save_to_disk_ignore_existing_entry_types(); test_file_write_to_disk_basic(); test_file_write_to_disk_creates_dirs(); test_file_write_to_disk_does_not_follow_symlink();