|
|
#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(); |
|
|
} |