#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include #include #include /* The following globals and function are defined in the ln source prior to this include: - static bool do_link(...) - static enum backup_type backup_type; - static bool symbolic_link, relative, logical, interactive, remove_existing_files, verbose, hard_dir_link, beware_hard_dir_link, dereference_dest_dir_symlinks; - static Hash_table *dest_set; They are accessible here since this file is included into the same translation unit. */ static char *path_join2(const char *a, const char *b) { size_t la = strlen(a); size_t lb = strlen(b); int need_slash = (la > 0 && a[la-1] != '/'); size_t len = la + (need_slash ? 1 : 0) + lb + 1; char *out = (char*)malloc(len); if (!out) return NULL; memcpy(out, a, la); if (need_slash) out[la++] = '/'; memcpy(out + la, b, lb); out[la + lb] = '\0'; return out; } static char *mkdtemp_wrap(void) { char tmpl[PATH_MAX]; const char *tmpdir = getenv("TMPDIR"); if (!tmpdir || !*tmpdir) tmpdir = "/tmp"; snprintf(tmpl, sizeof(tmpl), "%s/ln_do_link_test_%ld_XXXXXX", tmpdir, (long)getpid()); 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 *data) { int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd < 0) return -1; size_t len = strlen(data); ssize_t wr = write(fd, data, len); int e = 0; if (wr < 0 || (size_t)wr != len) e = -1; if (close(fd) != 0) e = -1; return e; } static int open_dirfd(const char *dir) { int fd = open(dir, O_RDONLY #ifdef O_DIRECTORY | O_DIRECTORY #endif #ifdef O_CLOEXEC | O_CLOEXEC #endif ); return fd; } static char *realpath_dup(const char *p) { char buf[PATH_MAX]; if (!realpath(p, buf)) return NULL; return strdup(buf); } /* Resolve the symlink text as it would be interpreted from dest_dir. */ static char *resolve_link_from_dir(const char *dest_dir, const char *link_text) { char *abs = NULL; if (link_text[0] == '/') { abs = realpath_dup(link_text); } else { char *joined = path_join2(dest_dir, link_text); if (!joined) return NULL; abs = realpath_dup(joined); free(joined); } return abs; } /* Reset all relevant ln flags and state to a known baseline. */ static void reset_ln_flags(void) { symbolic_link = false; relative = false; logical = !!LINK_FOLLOWS_SYMLINKS; /* use default as baseline; individual tests can set explicitly */ interactive = false; remove_existing_files = false; verbose = false; hard_dir_link = false; beware_hard_dir_link = false; dereference_dest_dir_symlinks = true; backup_type = no_backups; dest_set = NULL; } void setUp(void) { /* Nothing global to set here; tests call reset_ln_flags explicitly. */ } void tearDown(void) { /* Each test cleans up its own files/directories. */ } /* Test: simple hard link creation when destination does not exist. */ void test_do_link_hardlink_simple(void) { reset_ln_flags(); logical = 0; /* physical by default */ char *td = mkdtemp_wrap(); TEST_ASSERT_NOT_NULL(td); char *src = path_join2(td, "src.txt"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_EQUAL_INT(0, write_file(src, "hello")); char *dest_dir = td; /* place link in same directory */ char *dest_full = path_join2(dest_dir, "dest.hard"); TEST_ASSERT_NOT_NULL(dest_full); char *slash = strrchr(dest_full, '/'); TEST_ASSERT_NOT_NULL(slash); char *dest_base = slash + 1; int dfd = open_dirfd(dest_dir); TEST_ASSERT_TRUE(dfd >= 0); bool ok = do_link(src, dfd, dest_base, dest_full, -1); TEST_ASSERT_TRUE(ok); struct stat ss, ds; TEST_ASSERT_EQUAL_INT(0, stat(src, &ss)); TEST_ASSERT_EQUAL_INT(0, stat(dest_full, &ds)); TEST_ASSERT_EQUAL_UINT64(ss.st_ino, ds.st_ino); TEST_ASSERT_EQUAL_INT(ss.st_dev, ds.st_dev); close(dfd); unlink(dest_full); unlink(src); rmdir(td); free(dest_full); free(src); free(td); } /* Test: simple symlink creation with absolute target (non-relative). */ void test_do_link_symlink_absolute(void) { reset_ln_flags(); symbolic_link = true; relative = false; char *td = mkdtemp_wrap(); TEST_ASSERT_NOT_NULL(td); char *src = path_join2(td, "src.txt"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_EQUAL_INT(0, write_file(src, "world")); char *dest_full = path_join2(td, "dest.sym"); TEST_ASSERT_NOT_NULL(dest_full); char *slash = strrchr(dest_full, '/'); TEST_ASSERT_NOT_NULL(slash); char *dest_base = slash + 1; int dfd = open_dirfd(td); TEST_ASSERT_TRUE(dfd >= 0); bool ok = do_link(src, dfd, dest_base, dest_full, -1); TEST_ASSERT_TRUE(ok); char buf[PATH_MAX]; ssize_t n = readlink(dest_full, buf, sizeof(buf)-1); TEST_ASSERT_TRUE(n >= 0); buf[n] = '\0'; TEST_ASSERT_EQUAL_STRING(src, buf); close(dfd); unlink(dest_full); unlink(src); rmdir(td); free(dest_full); free(src); free(td); } /* Test: relative symlink creation (-r) and that it resolves to the correct absolute path. */ void test_do_link_symlink_relative_resolves(void) { reset_ln_flags(); symbolic_link = true; relative = true; char *td = mkdtemp_wrap(); TEST_ASSERT_NOT_NULL(td); /* Create source in td/a/srcdir/s.txt and dest in td/b/destdir/l.lnk */ char *a = path_join2(td, "a"); char *b = path_join2(td, "b"); char *srcdir = path_join2(a, "srcdir"); char *destdir = path_join2(b, "destdir"); TEST_ASSERT_EQUAL_INT(0, mkdir(a, 0700)); TEST_ASSERT_EQUAL_INT(0, mkdir(b, 0700)); TEST_ASSERT_EQUAL_INT(0, mkdir(srcdir, 0700)); TEST_ASSERT_EQUAL_INT(0, mkdir(destdir, 0700)); char *src = path_join2(srcdir, "s.txt"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_EQUAL_INT(0, write_file(src, "data")); char *dest_full = path_join2(destdir, "l.lnk"); TEST_ASSERT_NOT_NULL(dest_full); char *slash = strrchr(dest_full, '/'); TEST_ASSERT_NOT_NULL(slash); char *dest_base = slash + 1; int dfd = open_dirfd(destdir); TEST_ASSERT_TRUE(dfd >= 0); bool ok = do_link(src, dfd, dest_base, dest_full, -1); TEST_ASSERT_TRUE(ok); /* Read link text and ensure it resolves to the same abs path as src */ char linkbuf[PATH_MAX]; ssize_t n = readlink(dest_full, linkbuf, sizeof(linkbuf)-1); TEST_ASSERT_TRUE(n >= 0); linkbuf[n] = '\0'; char *src_abs = realpath_dup(src); TEST_ASSERT_NOT_NULL(src_abs); char *resolved = resolve_link_from_dir(destdir, linkbuf); TEST_ASSERT_NOT_NULL(resolved); TEST_ASSERT_EQUAL_STRING(src_abs, resolved); close(dfd); /* Cleanup */ unlink(dest_full); unlink(src); rmdir(destdir); rmdir(srcdir); rmdir(b); rmdir(a); rmdir(td); free(resolved); free(src_abs); free(dest_full); free(src); free(destdir); free(srcdir); free(b); free(a); free(td); } /* Test: force overwrite existing destination (-f). */ void test_do_link_force_overwrite(void) { reset_ln_flags(); symbolic_link = false; remove_existing_files = true; logical = 0; char *td = mkdtemp_wrap(); TEST_ASSERT_NOT_NULL(td); char *src = path_join2(td, "src.txt"); char *dest_full = path_join2(td, "dest.txt"); TEST_ASSERT_NOT_NULL(src && dest_full); TEST_ASSERT_EQUAL_INT(0, write_file(src, "new")); TEST_ASSERT_EQUAL_INT(0, write_file(dest_full, "old")); char *slash = strrchr(dest_full, '/'); TEST_ASSERT_NOT_NULL(slash); char *dest_base = slash + 1; int dfd = open_dirfd(td); TEST_ASSERT_TRUE(dfd >= 0); bool ok = do_link(src, dfd, dest_base, dest_full, -1); TEST_ASSERT_TRUE(ok); struct stat ss, ds; TEST_ASSERT_EQUAL_INT(0, stat(src, &ss)); TEST_ASSERT_EQUAL_INT(0, stat(dest_full, &ds)); TEST_ASSERT_EQUAL_UINT64(ss.st_ino, ds.st_ino); TEST_ASSERT_EQUAL_INT(ss.st_dev, ds.st_dev); close(dfd); unlink(dest_full); unlink(src); rmdir(td); free(dest_full); free(src); free(td); } /* Test: hard link to a directory is disallowed unless hard_dir_link is true. */ void test_do_link_hardlink_directory_disallowed(void) { reset_ln_flags(); symbolic_link = false; hard_dir_link = false; char *td = mkdtemp_wrap(); TEST_ASSERT_NOT_NULL(td); char *srcdir = path_join2(td, "adir"); char *dest_full = path_join2(td, "dest_dir_hard"); TEST_ASSERT_NOT_NULL(srcdir && dest_full); TEST_ASSERT_EQUAL_INT(0, mkdir(srcdir, 0700)); char *slash = strrchr(dest_full, '/'); TEST_ASSERT_NOT_NULL(slash); char *dest_base = slash + 1; int dfd = open_dirfd(td); TEST_ASSERT_TRUE(dfd >= 0); bool ok = do_link(srcdir, dfd, dest_base, dest_full, -1); TEST_ASSERT_FALSE(ok); /* Ensure destination was not created (should not exist) */ struct stat st; TEST_ASSERT_EQUAL_INT(-1, lstat(dest_full, &st)); TEST_ASSERT_EQUAL_INT(ENOENT, errno); close(dfd); rmdir(srcdir); rmdir(td); free(dest_full); free(srcdir); free(td); } /* Test: prevent self-overwrite when source and dest are the same entry with -f. */ void test_do_link_same_file_protection(void) { reset_ln_flags(); symbolic_link = false; remove_existing_files = true; /* required to trigger the self-same check */ char *td = mkdtemp_wrap(); TEST_ASSERT_NOT_NULL(td); char *path = path_join2(td, "same.txt"); TEST_ASSERT_NOT_NULL(path); TEST_ASSERT_EQUAL_INT(0, write_file(path, "x")); /* dest is the same as source */ char *dest_full = strdup(path); TEST_ASSERT_NOT_NULL(dest_full); char *slash = strrchr(dest_full, '/'); TEST_ASSERT_NOT_NULL(slash); char *dest_base = slash + 1; int dfd = open_dirfd(td); TEST_ASSERT_TRUE(dfd >= 0); bool ok = do_link(path, dfd, dest_base, dest_full, -1); TEST_ASSERT_FALSE(ok); close(dfd); unlink(path); rmdir(td); free(dest_full); free(path); free(td); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_do_link_hardlink_simple); RUN_TEST(test_do_link_symlink_absolute); RUN_TEST(test_do_link_symlink_relative_resolves); RUN_TEST(test_do_link_force_overwrite); RUN_TEST(test_do_link_hardlink_directory_disallowed); RUN_TEST(test_do_link_same_file_protection); return UNITY_END(); }