#include "test_delay_updates.h" #include "config.h" #include "delay_updates.h" #include "file.h" #include "file_receive.h" #include "test_utils.h" #include "utils.h" #include #include #include #include #include #include /* Recursively remove a test tree (never follows symlinks). */ static void remove_tree(const char* path) { struct stat st; if (lstat(path, &st) != 0) return; if (S_ISDIR(st.st_mode)) { DIR* dir = opendir(path); if (!dir) return; const struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; char* child = path_cat(path, entry->d_name); if (child) { remove_tree(child); free(child); } } closedir(dir); rmdir(path); } else { unlink(path); } } /* Build a File that carries `content`. */ static File* make_file(const char* path, const char* content) { File* f = file_create(path); if (!f) return NULL; f->data->data = malloc(strlen(content)); if (!f->data->data) { file_destroy(f); return NULL; } memcpy(f->data->data, content, strlen(content)); f->data->size = strlen(content); return f; } static char* read_all(const char* path) { FILE* fp = fopen(path, "rb"); if (!fp) return NULL; char buf[256] = {0}; size_t n = fread(buf, 1, sizeof(buf) - 1, fp); fclose(fp); char* out = malloc(n + 1); if (!out) return NULL; memcpy(out, buf, n); out[n] = '\0'; return out; } static void test_delay_updates_no_final_before_publish() { const char* root = "test_delay_tmp"; remove_tree(root); Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); cfg->delay_updates = true; File* f = make_file("sub/file.txt", "staged payload"); EXPECT_NOT_NULL(f); // cppcheck-suppress knownConditionTrueFalse if (!cfg || !f) goto out; EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN); EXPECT_NOT_NULL(cfg->delay_context); const char* final_path = "test_delay_tmp/sub/file.txt"; /* Before publication the final destination must not contain the file. */ EXPECT_FALSE(file_path_exists_secure(final_path)); /* The complete staged copy must live inside the per-run staging tree. */ EXPECT_NOT_NULL(cfg->delay_context->staging_name); EXPECT_EQ_INT(strncmp(cfg->delay_context->staging_name, ".fastsync-stage.", 16), 0); char* staged = path_cat(cfg->delay_context->staging_root, "/sub/file.txt"); EXPECT_NOT_NULL(staged); // cppcheck-suppress knownConditionTrueFalse if (staged) { char* content = read_all(staged); EXPECT_NOT_NULL(content); // cppcheck-suppress knownConditionTrueFalse if (content) { EXPECT_EQ_STR(content, "staged payload"); free(content); } free(staged); } out: file_destroy(f); config_delete(cfg); remove_tree(root); } static void test_delay_updates_publish_installs_files() { const char* root = "test_delay_pub_tmp"; remove_tree(root); Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); cfg->delay_updates = true; File* f = make_file("sub/file.txt", "published payload"); EXPECT_NOT_NULL(f); // cppcheck-suppress knownConditionTrueFalse if (!cfg || !f) goto out; EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN); const char* final_path = "test_delay_pub_tmp/sub/file.txt"; EXPECT_FALSE(file_path_exists_secure(final_path)); char* staging_root = str_dup(cfg->delay_context->staging_root); EXPECT_NOT_NULL(staging_root); EXPECT_TRUE(delay_updates_publish(cfg->delay_context, cfg)); /* After a successful publish the file is installed and staging is gone. */ char* content = read_all(final_path); EXPECT_NOT_NULL(content); // cppcheck-suppress knownConditionTrueFalse if (content) { EXPECT_EQ_STR(content, "published payload"); free(content); } EXPECT_FALSE(file_path_exists_secure(staging_root)); free(staging_root); out: file_destroy(f); config_delete(cfg); remove_tree(root); } /* The staged tree is cleaned on the error/abort path and final files that were never published do not appear at the destination. */ static void test_delay_updates_cleanup_removes_staged() { const char* root = "test_delay_clean_tmp"; remove_tree(root); Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); cfg->delay_updates = true; File* f = make_file("sub/file.txt", "never installed"); EXPECT_NOT_NULL(f); // cppcheck-suppress knownConditionTrueFalse if (!cfg || !f) goto out; EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN); char* staged_file = path_cat(cfg->delay_context->staging_root, "/sub/file.txt"); char* staging_root = str_dup(cfg->delay_context->staging_root); EXPECT_NOT_NULL(staged_file); EXPECT_NOT_NULL(staging_root); EXPECT_TRUE(file_path_exists_secure(staged_file)); delay_updates_cleanup(cfg->delay_context); EXPECT_FALSE(file_path_exists_secure(staging_root)); EXPECT_FALSE(file_path_exists_secure("test_delay_clean_tmp/sub/file.txt")); free(staged_file); free(staging_root); out: file_destroy(f); config_delete(cfg); remove_tree(root); } /* With --backup the previous version is only moved aside at publication. */ static void test_delay_updates_backup_deferred_to_publish() { const char* root = "test_delay_bak_tmp"; remove_tree(root); Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); cfg->delay_updates = true; cfg->backup = true; EXPECT_TRUE(file_write_to_disk("test_delay_bak_tmp/file.txt", "AAAA", 4, false, false)); File* f = make_file("file.txt", "BBBB"); EXPECT_NOT_NULL(f); // cppcheck-suppress knownConditionTrueFalse if (!cfg || !f) goto out; EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN); /* Stage time must not touch the final file or create the backup yet. */ char* before = read_all("test_delay_bak_tmp/file.txt"); EXPECT_NOT_NULL(before); // cppcheck-suppress knownConditionTrueFalse if (before) { EXPECT_EQ_STR(before, "AAAA"); free(before); } EXPECT_FALSE(file_path_exists_secure("test_delay_bak_tmp/file.txt~")); EXPECT_TRUE(delay_updates_publish(cfg->delay_context, cfg)); char* after = read_all("test_delay_bak_tmp/file.txt"); char* backup = read_all("test_delay_bak_tmp/file.txt~"); EXPECT_NOT_NULL(after); EXPECT_NOT_NULL(backup); // cppcheck-suppress knownConditionTrueFalse if (after) { EXPECT_EQ_STR(after, "BBBB"); free(after); } // cppcheck-suppress knownConditionTrueFalse if (backup) { EXPECT_EQ_STR(backup, "AAAA"); free(backup); } out: file_destroy(f); config_delete(cfg); remove_tree(root); } /* Skip/update policy checks run against the final path at stage time, matching what an immediate run would decide. */ static void test_delay_updates_skip_semantics() { const char* root = "test_delay_skip_tmp"; remove_tree(root); Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); cfg->delay_updates = true; /* --existing: final destination missing -> skipped, nothing staged. */ File* missing = make_file("missing.txt", "new"); EXPECT_NOT_NULL(missing); // cppcheck-suppress knownConditionTrueFalse if (!cfg || !missing) goto out; cfg->existing = true; EXPECT_EQ_INT(file_save_to_disk_full(root, missing, cfg), FILE_SAVE_SKIPPED); cfg->existing = false; /* --ignore-existing: final destination present -> skipped. */ EXPECT_TRUE(file_write_to_disk("test_delay_skip_tmp/existing.txt", "old", 3, false, false)); File* present = make_file("existing.txt", "new"); EXPECT_NOT_NULL(present); // cppcheck-suppress knownConditionTrueFalse if (!present) goto out; cfg->ignore_existing = true; EXPECT_EQ_INT(file_save_to_disk_full(root, present, cfg), FILE_SAVE_SKIPPED); cfg->ignore_existing = false; /* Without a skip flag the file is staged and later published. */ File* fresh = make_file("fresh.txt", "content"); EXPECT_NOT_NULL(fresh); // cppcheck-suppress knownConditionTrueFalse if (!fresh) goto out; EXPECT_EQ_INT(file_save_to_disk_full(root, fresh, cfg), FILE_SAVE_WRITTEN); EXPECT_TRUE(delay_updates_publish(cfg->delay_context, cfg)); char* content = read_all("test_delay_skip_tmp/fresh.txt"); EXPECT_NOT_NULL(content); // cppcheck-suppress knownConditionTrueFalse if (content) { EXPECT_EQ_STR(content, "content"); free(content); } out: file_destroy(missing); file_destroy(present); file_destroy(fresh); config_delete(cfg); remove_tree(root); } /* Every context picks its own staging directory name, so two delayed transfers to the same root can never share (and corrupt) a staging tree. */ static void test_delay_updates_unique_staging_name() { DelayUpdatesContext* first = delay_updates_context_create("test_delay_uniq_tmp"); DelayUpdatesContext* second = delay_updates_context_create("test_delay_uniq_tmp"); EXPECT_NOT_NULL(first); EXPECT_NOT_NULL(second); /* cppcheck-suppress knownConditionTrueFalse -- the EXPECT_NOT_NULL checks above return on NULL */ if (first && second) { EXPECT_EQ_INT(strncmp(first->staging_name, ".fastsync-stage.", 16), 0); EXPECT_EQ_INT(strncmp(second->staging_name, ".fastsync-stage.", 16), 0); EXPECT_TRUE(strcmp(first->staging_name, second->staging_name) != 0); EXPECT_TRUE(strcmp(first->staging_root, second->staging_root) != 0); } delay_updates_context_destroy(first); delay_updates_context_destroy(second); } /* A pre-existing destination entry at the exact (random) staging path is not ours: prepare() must refuse rather than wipe it. */ static void test_delay_updates_prepare_refuses_non_owned_collision() { const char* root = "test_delay_collide_tmp"; remove_tree(root); DelayUpdatesContext* context = delay_updates_context_create(root); EXPECT_NOT_NULL(context); // cppcheck-suppress knownConditionTrueFalse if (!context) return; /* Plant a genuine directory with user data at the exact staging path. */ EXPECT_TRUE(file_ensure_directory_secure(context->staging_root)); char* inner = path_cat(context->staging_root, "keepme.txt"); EXPECT_NOT_NULL(inner); // cppcheck-suppress knownConditionTrueFalse if (inner) { EXPECT_TRUE(file_write_to_disk(inner, "genuine", 7, false, false)); EXPECT_FALSE(delay_updates_prepare(context)); char* content = read_all(inner); EXPECT_NOT_NULL(content); // cppcheck-suppress knownConditionTrueFalse if (content) { EXPECT_EQ_STR(content, "genuine"); free(content); } free(inner); } delay_updates_context_destroy(context); remove_tree(root); } /* The reserved staging name must be recognizable for validation, including with a trailing slash. */ static void test_delay_updates_reserved_name_helper() { EXPECT_TRUE(delay_updates_staging_name_conflict(".fastsync-stage")); EXPECT_TRUE(delay_updates_staging_name_conflict(".fastsync-stage/")); EXPECT_TRUE(delay_updates_staging_name_conflict(".fastsync-stage///")); EXPECT_FALSE(delay_updates_staging_name_conflict(NULL)); EXPECT_FALSE(delay_updates_staging_name_conflict("")); EXPECT_FALSE(delay_updates_staging_name_conflict("backups")); EXPECT_FALSE(delay_updates_staging_name_conflict(".fastsync-stage.bak")); } void test_delay_updates() { test_delay_updates_reserved_name_helper(); test_delay_updates_unique_staging_name(); test_delay_updates_prepare_refuses_non_owned_collision(); test_delay_updates_no_final_before_publish(); test_delay_updates_publish_installs_files(); test_delay_updates_cleanup_removes_staged(); test_delay_updates_backup_deferred_to_publish(); test_delay_updates_skip_semantics(); }