#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include /* Globals from the program (accessible since this file is included into the TU): - struct mode_change *change; - mode_t umask_value; - bool recurse; - int dereference; - bool force_silent; - bool diagnose_surprises; - enum Verbosity verbosity; - Functions: mode_compile, xfts_open, fts_read, fts_set, fts_close, process_file */ static mode_t saved_umask; /* Utility: create a temporary directory, return malloc'd path */ static char* make_temp_dir(void) { char tmpl[] = "/tmp/chmod_ut_XXXXXX"; char *path = malloc(sizeof(tmpl)); if (!path) return NULL; memcpy(path, tmpl, sizeof(tmpl)); if (!mkdtemp(path)) { free(path); return NULL; } return path; } /* Utility: join dir and name into a newly malloc'd path */ static char* path_join(const char* dir, const char* name) { size_t ld = strlen(dir), ln = strlen(name); size_t need = ld + 1 + ln + 1; char *res = malloc(need); if (!res) return NULL; memcpy(res, dir, ld); res[ld] = '/'; memcpy(res + ld + 1, name, ln); res[ld + 1 + ln] = '\0'; return res; } /* Utility: create a regular file with specified mode, returns 0 on success */ static int create_file_with_mode(const char* path, mode_t mode) { int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, mode); if (fd < 0) return -1; if (close(fd) != 0) return -1; /* Ensure exact mode (umask 0 in setUp, but call chmod just in case) */ if (chmod(path, mode) != 0) return -1; return 0; } /* Utility: create symlink */ static int create_symlink_to(const char* target, const char* linkpath) { /* Remove pre-existing */ unlink(linkpath); return symlink(target, linkpath); } /* Utility: stat mode of a path (referent) */ static mode_t get_mode_stat(const char* path) { struct stat st; if (stat(path, &st) != 0) return (mode_t)~0; return st.st_mode; } /* Utility: lstat mode of a path (symlink itself) */ static mode_t get_mode_lstat(const char* path) { struct stat st; if (lstat(path, &st) != 0) return (mode_t)~0; return st.st_mode; } /* Utility: open xfts for a single path and return first entry */ static FTSENT* open_single_fts(const char* path, FTS **out_fts, int flags) { char *paths[2]; paths[0] = (char*)path; paths[1] = NULL; FTS *fts = xfts_open(paths, flags, NULL); if (!fts) { *out_fts = NULL; return NULL; } FTSENT *ent = fts_read(fts); *out_fts = fts; return ent; } void setUp(void) { /* Ensure environment messages are English for stable assertions */ setenv("LC_ALL", "C", 1); setenv("LANG", "C", 1); /* Make umask predictable */ saved_umask = umask(0); umask_value = 0; /* Defaults for globals used by process_file */ recurse = false; dereference = -1; /* default */ force_silent = false; diagnose_surprises = false; verbosity = V_off; } void tearDown(void) { /* Restore process umask */ umask(saved_umask); } /* Test 1: Regular file mode change should succeed and set execute for user */ void test_process_file_regular_add_exec(void) { char *dir = make_temp_dir(); TEST_ASSERT_NOT_NULL(dir); char *file = path_join(dir, "f1"); TEST_ASSERT_NOT_NULL(file); TEST_ASSERT_EQUAL_INT(0, create_file_with_mode(file, 0644)); /* Prepare change: add u+x */ change = mode_compile("u+x", 0); FTS *fts = NULL; FTSENT *ent = open_single_fts(file, &fts, FTS_PHYSICAL); TEST_ASSERT_NOT_NULL_MESSAGE(ent, "fts_read should return an entry for the file"); TEST_ASSERT_NOT_NULL(fts); bool ok = process_file(fts, ent); /* Close FTS before assertions further to avoid side effects */ TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); TEST_ASSERT_TRUE(ok); mode_t m = get_mode_stat(file); TEST_ASSERT_NOT_EQUAL((mode_t)~0, m); TEST_ASSERT_TRUE(m & S_IXUSR); unlink(file); rmdir(dir); free(file); free(dir); } /* Test 2: Symlink with no-dereference should not change referent */ void test_process_file_symlink_no_deref_noop(void) { char *dir = make_temp_dir(); TEST_ASSERT_NOT_NULL(dir); char *tgt = path_join(dir, "target"); char *lnk = path_join(dir, "link"); TEST_ASSERT_NOT_NULL(tgt); TEST_ASSERT_NOT_NULL(lnk); TEST_ASSERT_EQUAL_INT(0, create_file_with_mode(tgt, 0644)); TEST_ASSERT_EQUAL_INT(0, create_symlink_to(tgt, lnk)); mode_t before = get_mode_stat(tgt); TEST_ASSERT_NOT_EQUAL((mode_t)~0, before); /* change: a+x, but do not dereference symlinks */ change = mode_compile("a+x", 0); dereference = 0; /* --no-dereference */ FTS *fts = NULL; FTSENT *ent = open_single_fts(lnk, &fts, FTS_PHYSICAL); TEST_ASSERT_NOT_NULL(ent); TEST_ASSERT_NOT_NULL(fts); bool ok = process_file(fts, ent); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); TEST_ASSERT_TRUE(ok); mode_t after = get_mode_stat(tgt); TEST_ASSERT_NOT_EQUAL((mode_t)~0, after); TEST_ASSERT_EQUAL_HEX32(before & 07777, after & 07777); unlink(lnk); unlink(tgt); rmdir(dir); free(lnk); free(tgt); free(dir); } /* Test 3: Symlink with dereference should change the referent */ void test_process_file_symlink_with_deref_changes_target(void) { char *dir = make_temp_dir(); TEST_ASSERT_NOT_NULL(dir); char *tgt = path_join(dir, "target2"); char *lnk = path_join(dir, "link2"); TEST_ASSERT_NOT_NULL(tgt); TEST_ASSERT_NOT_NULL(lnk); TEST_ASSERT_EQUAL_INT(0, create_file_with_mode(tgt, 0644)); TEST_ASSERT_EQUAL_INT(0, create_symlink_to(tgt, lnk)); change = mode_compile("a+x", 0); dereference = 1; /* --dereference */ verbosity = V_off; FTS *fts = NULL; FTSENT *ent = open_single_fts(lnk, &fts, FTS_PHYSICAL); TEST_ASSERT_NOT_NULL(ent); TEST_ASSERT_NOT_NULL(fts); bool ok = process_file(fts, ent); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); TEST_ASSERT_TRUE(ok); mode_t m = get_mode_stat(tgt); TEST_ASSERT_NOT_EQUAL((mode_t)~0, m); TEST_ASSERT_TRUE(m & S_IXUSR); unlink(lnk); unlink(tgt); rmdir(dir); free(lnk); free(tgt); free(dir); } /* Test 4: No-op change when mode already has requested bits */ void test_process_file_no_change_needed(void) { char *dir = make_temp_dir(); TEST_ASSERT_NOT_NULL(dir); char *file = path_join(dir, "already_exec"); TEST_ASSERT_NOT_NULL(file); TEST_ASSERT_EQUAL_INT(0, create_file_with_mode(file, 0755)); mode_t before = get_mode_stat(file); TEST_ASSERT_NOT_EQUAL((mode_t)~0, before); change = mode_compile("u+x", 0); verbosity = V_off; FTS *fts = NULL; FTSENT *ent = open_single_fts(file, &fts, FTS_PHYSICAL); TEST_ASSERT_NOT_NULL(ent); TEST_ASSERT_NOT_NULL(fts); bool ok = process_file(fts, ent); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); TEST_ASSERT_TRUE(ok); mode_t after = get_mode_stat(file); TEST_ASSERT_NOT_EQUAL((mode_t)~0, after); TEST_ASSERT_EQUAL_HEX32(before & 07777, after & 07777); unlink(file); rmdir(dir); free(file); free(dir); } /* Test 5: Verbosity high prints a diagnostic for a change */ void test_process_file_verbose_outputs_message(void) { char *dir = make_temp_dir(); TEST_ASSERT_NOT_NULL(dir); char *file = path_join(dir, "verbose_file"); TEST_ASSERT_NOT_NULL(file); TEST_ASSERT_EQUAL_INT(0, create_file_with_mode(file, 0644)); change = mode_compile("u+x", 0); verbosity = V_high; /* Setup stdout capture */ int saved_stdout = dup(STDOUT_FILENO); TEST_ASSERT_MESSAGE(saved_stdout >= 0, "dup stdout failed"); int pipefd[2]; TEST_ASSERT_EQUAL_INT_MESSAGE(0, pipe(pipefd), "pipe failed"); fflush(stdout); if (dup2(pipefd[1], STDOUT_FILENO) < 0) { /* Restore and fail */ dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); close(pipefd[0]); close(pipefd[1]); TEST_FAIL_MESSAGE("dup2 failed to redirect stdout"); } FTS *fts = NULL; FTSENT *ent = open_single_fts(file, &fts, FTS_PHYSICAL); TEST_ASSERT_NOT_NULL(ent); TEST_ASSERT_NOT_NULL(fts); /* Call target while stdout is redirected. Do not use Unity asserts here. */ bool ok_call = process_file(fts, ent); ignore_value(fts_close(fts)); /* Close FTS even if we don't assert now */ fflush(stdout); /* Restore stdout */ dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); close(pipefd[1]); /* Now safe to assert */ TEST_ASSERT_TRUE(ok_call); char buf[1024]; ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); close(pipefd[0]); TEST_ASSERT_TRUE_MESSAGE(n > 0, "no output captured"); if (n > 0) buf[n] = '\0'; else buf[0] = '\0'; /* Expect message like: "mode of changed from ..." */ TEST_ASSERT_NOT_NULL_MESSAGE(strstr(buf, "mode of"), "Verbose output missing 'mode of'"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(buf, "changed"), "Verbose output missing 'changed'"); unlink(file); rmdir(dir); free(file); free(dir); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_process_file_regular_add_exec); RUN_TEST(test_process_file_symlink_no_deref_noop); RUN_TEST(test_process_file_symlink_with_deref_changes_target); RUN_TEST(test_process_file_no_change_needed); RUN_TEST(test_process_file_verbose_outputs_message); return UNITY_END(); }