1 /* 2 * Copyright (c) 2026 Jitendra Bhati 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7 /* 8 * Tests for fts_open() error conditions and edge cases. 9 */ 10 11 #include <sys/stat.h> 12 13 #include <errno.h> 14 #include <fcntl.h> 15 #include <fts.h> 16 #include <stdbool.h> 17 #include <stdio.h> 18 #include <stdlib.h> 19 #include <unistd.h> 20 21 #include <atf-c.h> 22 23 #include "fts_test.h" 24 25 /* 26 * Option bits outside FTS_OPTIONMASK must fail with EINVAL. 27 */ 28 ATF_TC(invalid_options); 29 ATF_TC_HEAD(invalid_options, tc) 30 { 31 atf_tc_set_md_var(tc, "descr", 32 "fts_open with out-of-mask option bits fails with EINVAL"); 33 } 34 ATF_TC_BODY(invalid_options, tc) 35 { 36 char *paths[] = { ".", NULL }; 37 38 ATF_REQUIRE_ERRNO(EINVAL, fts_open(paths, 0x10000, NULL) == NULL); 39 } 40 41 /* 42 * Empty argv (NULL as first element) must fail with EINVAL. 43 */ 44 ATF_TC(empty_argv); 45 ATF_TC_HEAD(empty_argv, tc) 46 { 47 atf_tc_set_md_var(tc, "descr", 48 "fts_open with NULL first argv element fails with EINVAL"); 49 } 50 ATF_TC_BODY(empty_argv, tc) 51 { 52 char *paths[] = { NULL }; 53 54 ATF_REQUIRE_ERRNO(EINVAL, 55 fts_open(paths, FTS_PHYSICAL, NULL) == NULL); 56 } 57 58 /* 59 * An empty string in argv is a valid path but stat("") fails with ENOENT. 60 * fts_open() succeeds; the resulting FTSENT has fts_info == FTS_NS and 61 * fts_errno == ENOENT. 62 */ 63 ATF_TC(empty_path_string); 64 ATF_TC_HEAD(empty_path_string, tc) 65 { 66 atf_tc_set_md_var(tc, "descr", 67 "empty string in argv produces FTS_NS entry"); 68 } 69 ATF_TC_BODY(empty_path_string, tc) 70 { 71 char *paths[] = { "", NULL }; 72 FTS *fts; 73 FTSENT *ent; 74 75 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 76 77 ent = fts_read(fts); 78 ATF_REQUIRE(ent != NULL); 79 ATF_CHECK_EQ(FTS_NS, ent->fts_info); 80 ATF_CHECK_EQ(ENOENT, ent->fts_errno); 81 82 fts_close(fts); 83 } 84 85 /* 86 * A nonexistent path produces an FTS_NS entry rather than causing 87 * fts_open() itself to fail. fts_open() does not validate whether 88 * paths exist. errno must be 0 after the traversal ends normally. 89 */ 90 ATF_TC(nonexistent_path); 91 ATF_TC_HEAD(nonexistent_path, tc) 92 { 93 atf_tc_set_md_var(tc, "descr", 94 "nonexistent path produces FTS_NS entry, not fts_open failure"); 95 } 96 ATF_TC_BODY(nonexistent_path, tc) 97 { 98 char *paths[] = { "this-path-does-not-exist", NULL }; 99 FTS *fts; 100 FTSENT *ent; 101 102 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 103 104 ent = fts_read(fts); 105 ATF_REQUIRE(ent != NULL); 106 ATF_CHECK_EQ(FTS_NS, ent->fts_info); 107 ATF_CHECK_EQ(ENOENT, ent->fts_errno); 108 109 /* 110 * Next fts_read must return NULL with errno == 0 — 111 * end-of-traversal, not an error. 112 */ 113 errno = 1; /* sentinel — fts_read must clear this */ 114 ATF_CHECK_EQ(NULL, fts_read(fts)); 115 ATF_CHECK_EQ(0, errno); 116 117 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 118 } 119 120 /* 121 * A path with a trailing slash must not crash and must traverse the 122 * directory normally. This is a regression test for SVN r49851. 123 */ 124 ATF_TC(trailing_slash); 125 ATF_TC_HEAD(trailing_slash, tc) 126 { 127 atf_tc_set_md_var(tc, "descr", 128 "trailing slash on root path must not crash (SVN r49851)"); 129 } 130 ATF_TC_BODY(trailing_slash, tc) 131 { 132 char *paths[] = { "dir/", NULL }; 133 FTS *fts; 134 FTSENT *ent; 135 int seen_dir, seen_file; 136 137 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 138 ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); 139 140 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 141 142 seen_dir = 0; 143 seen_file = 0; 144 while ((ent = fts_read(fts)) != NULL) { 145 if (ent->fts_info == FTS_D || ent->fts_info == FTS_DP) 146 seen_dir = 1; 147 if (ent->fts_info == FTS_F) 148 seen_file = 1; 149 } 150 151 ATF_CHECK_EQ_MSG(0, errno, 152 "fts_read loop should end with errno 0, not %d", errno); 153 ATF_CHECK_MSG(seen_dir != 0, "directory was never visited"); 154 ATF_CHECK_MSG(seen_file != 0, "file inside dir was never visited"); 155 156 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 157 } 158 159 /* 160 * An unreadable directory must produce FTS_D then FTS_DNR. It must NOT 161 * produce FTS_DP because fts never successfully entered it. 162 * 163 * Requires an unprivileged user because root ignores directory permissions. 164 */ 165 ATF_TC(unreadable_dir); 166 ATF_TC_HEAD(unreadable_dir, tc) 167 { 168 atf_tc_set_md_var(tc, "descr", 169 "unreadable directory yields FTS_D then FTS_DNR, never FTS_DP"); 170 atf_tc_set_md_var(tc, "require.user", "unprivileged"); 171 } 172 ATF_TC_BODY(unreadable_dir, tc) 173 { 174 ATF_REQUIRE_EQ(0, mkdir("unr", 0000)); 175 fts_test(tc, &(struct fts_testcase){ 176 (char *[]){ "unr", NULL }, 177 FTS_PHYSICAL, 178 (struct fts_expect[]){ 179 { FTS_D, "unr", "unr" }, 180 { FTS_DNR, "unr", "unr" }, 181 { 0 } 182 }, 183 }); 184 } 185 186 /* 187 * Multiple root paths must all be visited left-to-right, each tree 188 * traversed completely before moving to the next root. 189 */ 190 ATF_TC(multiple_roots); 191 ATF_TC_HEAD(multiple_roots, tc) 192 { 193 atf_tc_set_md_var(tc, "descr", 194 "fts_open visits multiple root paths left-to-right"); 195 } 196 ATF_TC_BODY(multiple_roots, tc) 197 { 198 ATF_REQUIRE_EQ(0, mkdir("a", 0755)); 199 ATF_REQUIRE_EQ(0, mkdir("b", 0755)); 200 ATF_REQUIRE_EQ(0, close(creat("a/x", 0644))); 201 ATF_REQUIRE_EQ(0, close(creat("b/y", 0644))); 202 203 fts_test(tc, &(struct fts_testcase){ 204 (char *[]){ "a", "b", NULL }, 205 FTS_PHYSICAL, 206 (struct fts_expect[]){ 207 { FTS_D, "a", "a" }, 208 { FTS_F, "x", "x" }, 209 { FTS_DP, "a", "a" }, 210 { FTS_D, "b", "b" }, 211 { FTS_F, "y", "y" }, 212 { FTS_DP, "b", "b" }, 213 { 0 } 214 }, 215 }); 216 } 217 218 ATF_TP_ADD_TCS(tp) 219 { 220 fts_check_debug(); 221 ATF_TP_ADD_TC(tp, invalid_options); 222 ATF_TP_ADD_TC(tp, empty_argv); 223 ATF_TP_ADD_TC(tp, empty_path_string); 224 ATF_TP_ADD_TC(tp, nonexistent_path); 225 ATF_TP_ADD_TC(tp, trailing_slash); 226 ATF_TP_ADD_TC(tp, unreadable_dir); 227 ATF_TP_ADD_TC(tp, multiple_roots); 228 229 return (atf_no_error()); 230 } 231