#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include #include #include /* Helpers: filesystem utilities for tests */ static char *path_join(const char *a, const char *b) { size_t la = strlen(a); size_t lb = strlen(b); bool need_slash = (la > 0 && a[la-1] != '/'); size_t len = la + (need_slash ? 1 : 0) + lb + 1; char *res = (char *)malloc(len); if (!res) return NULL; strcpy(res, a); if (need_slash) strcat(res, "/"); strcat(res, b); return res; } static char *make_temp_dir(void) { char tmpl[] = "/tmp/mv_do_move_test_XXXXXX"; char *buf = strdup(tmpl); if (!buf) return NULL; if (!mkdtemp(buf)) { free(buf); return NULL; } return buf; /* caller must free with rm_rf or free if empty */ } static int write_file(const char *path, const char *content) { int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600); if (fd < 0) return -1; ssize_t len = (content ? (ssize_t)strlen(content) : 0); ssize_t w = 0; if (len > 0) { w = write(fd, content, (size_t)len); if (w != len) { int e = errno; close(fd); errno = e; return -1; } } if (close(fd) < 0) return -1; return 0; } static int read_file(const char *path, char *buf, size_t bufsz, ssize_t *out_len) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; ssize_t r = read(fd, buf, bufsz); int e = errno; close(fd); if (r < 0) { errno = e; return -1; } if (out_len) *out_len = r; return 0; } static int ensure_dir(const char *path, mode_t mode) { if (mkdir(path, mode) == 0) return 0; if (errno == EEXIST) return 0; return -1; } static bool path_exists(const char *path) { struct stat st; return stat(path, &st) == 0; } static bool is_dir(const char *path) { struct stat st; if (stat(path, &st) != 0) return false; return S_ISDIR(st.st_mode); } /* Recursive remove for cleanup */ static int rm_rf(const char *path) { struct stat st; if (lstat(path, &st) != 0) { if (errno == ENOENT) return 0; return -1; } if (S_ISDIR(st.st_mode)) { DIR *d = opendir(path); if (!d) return -1; struct dirent *de; while ((de = readdir(d)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; char *child = path_join(path, de->d_name); if (!child) { closedir(d); return -1; } if (rm_rf(child) != 0) { free(child); closedir(d); return -1; } free(child); } closedir(d); if (rmdir(path) != 0) return -1; } else { if (unlink(path) != 0) return -1; } return 0; } /* For content checks */ static void assert_file_has_content(const char *path, const char *expected) { char buf[4096]; ssize_t n = -1; TEST_ASSERT_TRUE_MESSAGE(path_exists(path), "Expected file to exist"); TEST_ASSERT_EQUAL_INT_MESSAGE(0, read_file(path, buf, sizeof buf, &n), "Failed to read file"); size_t exp_len = strlen(expected); TEST_ASSERT_EQUAL_size_t_MESSAGE(exp_len, (size_t)n, "File length mismatch"); TEST_ASSERT_EQUAL_INT_MESSAGE(0, memcmp(buf, expected, (size_t)n), "File content mismatch"); } /* Unity fixtures */ void setUp(void) { /* empty */ } void tearDown(void) { /* empty */ } /* Access do_move and cp_option_init provided in mv.c translation unit */ extern bool do_move(char const *source, char const *dest, int dest_dirfd, char const *dest_relname, const struct cp_options *x); /* cp_option_init is static in mv.c, but since this test file is included into the same translation unit, we can declare it here to call it. */ static void cp_option_init(struct cp_options *x); static void test_do_move_rename_file_same_dir(void) { char *tmp = make_temp_dir(); TEST_ASSERT_NOT_NULL(tmp); char *src = path_join(tmp, "a.txt"); char *dst = path_join(tmp, "b.txt"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst); TEST_ASSERT_EQUAL_INT(0, write_file(src, "hello world")); struct cp_options opt; cp_option_init(&opt); bool ok = do_move(src, dst, AT_FDCWD, dst, &opt); TEST_ASSERT_TRUE_MESSAGE(ok, "do_move should succeed for simple rename"); TEST_ASSERT_FALSE_MESSAGE(path_exists(src), "Source should be gone after rename"); assert_file_has_content(dst, "hello world"); free(src); free(dst); TEST_ASSERT_EQUAL_INT(0, rm_rf(tmp)); free(tmp); } static void test_do_move_rename_directory_into_dir(void) { char *tmp = make_temp_dir(); TEST_ASSERT_NOT_NULL(tmp); char *srcdir = path_join(tmp, "srcdir"); char *inner = path_join(srcdir, "file"); char *destroot = path_join(tmp, "destroot"); char *destpath = path_join(destroot, "srcdir"); TEST_ASSERT_NOT_NULL(srcdir && inner && destroot && destpath); TEST_ASSERT_EQUAL_INT(0, ensure_dir(srcdir, 0700)); TEST_ASSERT_EQUAL_INT(0, write_file(inner, "data")); TEST_ASSERT_EQUAL_INT(0, ensure_dir(destroot, 0700)); struct cp_options opt; cp_option_init(&opt); bool ok = do_move(srcdir, destpath, AT_FDCWD, destpath, &opt); TEST_ASSERT_TRUE_MESSAGE(ok, "Moving a directory into another directory should succeed"); TEST_ASSERT_FALSE_MESSAGE(path_exists(srcdir), "Source directory should be removed"); TEST_ASSERT_TRUE_MESSAGE(is_dir(destpath), "Destination directory should exist"); char *moved_inner = path_join(destpath, "file"); TEST_ASSERT_NOT_NULL(moved_inner); assert_file_has_content(moved_inner, "data"); free(moved_inner); free(srcdir); free(inner); free(destroot); free(destpath); TEST_ASSERT_EQUAL_INT(0, rm_rf(tmp)); free(tmp); } static void test_do_move_copy_into_self_directory_fails(void) { char *tmp = make_temp_dir(); TEST_ASSERT_NOT_NULL(tmp); char *parent = path_join(tmp, "parent"); char *child = path_join(parent, "child"); TEST_ASSERT_NOT_NULL(parent && child); TEST_ASSERT_EQUAL_INT(0, ensure_dir(parent, 0700)); TEST_ASSERT_EQUAL_INT(0, ensure_dir(child, 0700)); /* Try to move parent into its subdirectory: should fail */ struct cp_options opt; cp_option_init(&opt); bool ok = do_move(parent, child, AT_FDCWD, child, &opt); TEST_ASSERT_FALSE_MESSAGE(ok, "Moving a directory into its own subdirectory must fail"); /* Ensure original 'parent' still exists and we did not create a recursive loop */ TEST_ASSERT_TRUE_MESSAGE(is_dir(parent), "Parent directory should still exist"); /* Ensure child still exists and does not contain another 'parent' (avoid recursion) */ char *grand = path_join(child, "parent"); TEST_ASSERT_NOT_NULL(grand); TEST_ASSERT_FALSE_MESSAGE(path_exists(grand), "Should not create nested parent inside child"); free(grand); free(parent); free(child); TEST_ASSERT_EQUAL_INT(0, rm_rf(tmp)); free(tmp); } static void test_do_move_rename_fails_copy_then_rm_fails_due_to_perms(void) { char *tmp = make_temp_dir(); TEST_ASSERT_NOT_NULL(tmp); char *srcdir = path_join(tmp, "srcdir"); char *srcfile = path_join(srcdir, "file.txt"); char *dstfile = path_join(tmp, "dest.txt"); TEST_ASSERT_NOT_NULL(srcdir && srcfile && dstfile); TEST_ASSERT_EQUAL_INT(0, ensure_dir(srcdir, 0700)); TEST_ASSERT_EQUAL_INT(0, write_file(srcfile, "perm-test")); /* Remove write permission on source directory to block rename and later removal */ TEST_ASSERT_EQUAL_INT(0, chmod(srcdir, 0555)); struct cp_options opt; cp_option_init(&opt); bool ok = do_move(srcfile, dstfile, AT_FDCWD, dstfile, &opt); /* We expect failure since rm() should not be able to remove source due to permissions. */ TEST_ASSERT_FALSE_MESSAGE(ok, "do_move should fail when it cannot remove the source after copying"); /* Source should still exist (cannot remove due to directory perms) */ TEST_ASSERT_TRUE_MESSAGE(path_exists(srcfile), "Source file should still exist"); /* Destination may or may not exist depending on copy() behavior, but if it does, ensure content */ if (path_exists(dstfile)) { assert_file_has_content(dstfile, "perm-test"); } /* Restore permissions to allow cleanup */ TEST_ASSERT_EQUAL_INT(0, chmod(srcdir, 0700)); free(srcdir); free(srcfile); free(dstfile); TEST_ASSERT_EQUAL_INT(0, rm_rf(tmp)); free(tmp); } static void test_do_move_with_dest_dirfd_and_relname(void) { char *tmp = make_temp_dir(); TEST_ASSERT_NOT_NULL(tmp); char *src = path_join(tmp, "a"); char *dst_full = path_join(tmp, "rel_b"); TEST_ASSERT_NOT_NULL(src && dst_full); TEST_ASSERT_EQUAL_INT(0, write_file(src, "dirfd-test")); int dfd = open(tmp, O_RDONLY | O_DIRECTORY); TEST_ASSERT_TRUE_MESSAGE(dfd >= 0, "Failed to open directory fd"); struct cp_options opt; cp_option_init(&opt); /* Use dirfd with relative destination name */ bool ok = do_move(src, dst_full, dfd, "rel_b", &opt); TEST_ASSERT_TRUE_MESSAGE(ok, "do_move should succeed using dirfd/relname destination"); TEST_ASSERT_FALSE_MESSAGE(path_exists(src), "Source should be gone"); assert_file_has_content(dst_full, "dirfd-test"); close(dfd); free(src); free(dst_full); TEST_ASSERT_EQUAL_INT(0, rm_rf(tmp)); free(tmp); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_do_move_rename_file_same_dir); RUN_TEST(test_do_move_rename_directory_into_dir); RUN_TEST(test_do_move_copy_into_self_directory_fails); RUN_TEST(test_do_move_rename_fails_copy_then_rm_fails_due_to_perms); RUN_TEST(test_do_move_with_dest_dirfd_and_relname); return UNITY_END(); }