| #include "../../unity/unity.h" |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <stdbool.h> |
| #include <string.h> |
| #include <fcntl.h> |
| #include <unistd.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <dirent.h> |
| #include <errno.h> |
| #include <limits.h> |
|
|
| |
|
|
| 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; |
| } |
|
|
| 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); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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"); |
| } |
|
|
| |
| void setUp(void) { } |
| void tearDown(void) { } |
|
|
| |
| extern bool do_move(char const *source, char const *dest, |
| int dest_dirfd, char const *dest_relname, |
| const struct cp_options *x); |
|
|
| |
| |
| 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)); |
|
|
| |
| 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"); |
|
|
| |
| TEST_ASSERT_TRUE_MESSAGE(is_dir(parent), "Parent directory should still exist"); |
| |
| 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")); |
|
|
| |
| 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); |
|
|
| |
| TEST_ASSERT_FALSE_MESSAGE(ok, "do_move should fail when it cannot remove the source after copying"); |
|
|
| |
| TEST_ASSERT_TRUE_MESSAGE(path_exists(srcfile), "Source file should still exist"); |
|
|
| |
| if (path_exists(dstfile)) { |
| assert_file_has_content(dstfile, "perm-test"); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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(); |
| } |