1 /* 2 * Copyright (c) 2026 Jitendra Bhati 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7 /* 8 * Tests for fts_set(), fts_set_clientptr(), fts_get_clientptr(), 9 * and fts_get_stream(). 10 */ 11 12 #include <sys/stat.h> 13 14 #include <errno.h> 15 #include <fcntl.h> 16 #include <fts.h> 17 #include <stdbool.h> 18 #include <stdio.h> 19 #include <stdlib.h> 20 #include <string.h> 21 #include <unistd.h> 22 23 #include <atf-c.h> 24 25 /* 26 * fts_set with invalid options must return non-zero with EINVAL. 27 * Note: fts_set returns 1 (not -1) on error. 28 */ 29 ATF_TC(invalid_options); 30 ATF_TC_HEAD(invalid_options, tc) 31 { 32 atf_tc_set_md_var(tc, "descr", 33 "fts_set with invalid options returns non-zero with EINVAL"); 34 } 35 ATF_TC_BODY(invalid_options, tc) 36 { 37 char *paths[] = { ".", NULL }; 38 FTS *fts; 39 FTSENT *ent; 40 41 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 42 43 ent = fts_read(fts); 44 ATF_REQUIRE(ent != NULL); 45 ATF_REQUIRE_ERRNO(EINVAL, fts_set(fts, ent, 99) != 0); 46 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 47 } 48 49 /* 50 * FTS_AGAIN causes the current node to be re-stat()ed and returned 51 * again on the next fts_read() call. 52 */ 53 ATF_TC(again); 54 ATF_TC_HEAD(again, tc) 55 { 56 atf_tc_set_md_var(tc, "descr", 57 "FTS_AGAIN causes the current node to be returned once more"); 58 } 59 ATF_TC_BODY(again, tc) 60 { 61 char *paths[] = { "dir", NULL }; 62 FTS *fts; 63 FTSENT *ent; 64 int revisit_count; 65 66 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 67 ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); 68 69 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 70 71 revisit_count = 0; 72 for (errno = 0; (ent = fts_read(fts)) != NULL; errno = 0) { 73 if (ent->fts_info == FTS_F && revisit_count == 0) { 74 ATF_REQUIRE_EQ_MSG(0, 75 fts_set(fts, ent, FTS_AGAIN), 76 "fts_set(FTS_AGAIN): %m"); 77 revisit_count++; 78 } else if (ent->fts_info == FTS_F && revisit_count >= 1) { 79 revisit_count++; 80 } 81 } 82 ATF_CHECK_EQ_MSG(0, errno, "traversal ended with errno %d", errno); 83 ATF_CHECK_EQ_MSG(2, revisit_count, 84 "expected file visited twice via FTS_AGAIN, saw %d", 85 revisit_count); 86 87 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 88 } 89 90 /* 91 * FTS_AGAIN set twice in a row causes the node to be visited three 92 * times total. Each fts_read() clears fts_options, so the caller must 93 * set FTS_AGAIN again explicitly each time. 94 */ 95 ATF_TC(again_consecutive); 96 ATF_TC_HEAD(again_consecutive, tc) 97 { 98 atf_tc_set_md_var(tc, "descr", 99 "FTS_AGAIN set twice in a row visits the node three times"); 100 } 101 ATF_TC_BODY(again_consecutive, tc) 102 { 103 char *paths[] = { "file", NULL }; 104 FTS *fts; 105 FTSENT *ent; 106 int visit_count; 107 108 ATF_REQUIRE_EQ(0, close(creat("file", 0644))); 109 110 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 111 112 visit_count = 0; 113 while ((ent = fts_read(fts)) != NULL) { 114 if (ent->fts_info == FTS_F) { 115 visit_count++; 116 if (visit_count < 3) 117 ATF_REQUIRE_EQ(0, 118 fts_set(fts, ent, FTS_AGAIN)); 119 } 120 } 121 ATF_CHECK_EQ_MSG(3, visit_count, 122 "expected 3 visits with consecutive FTS_AGAIN, got %d", 123 visit_count); 124 125 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 126 } 127 128 /* 129 * FTS_FOLLOW on an FTS_SL entry pointing to a regular file yields FTS_F. 130 */ 131 ATF_TC(follow_symlink_to_file); 132 ATF_TC_HEAD(follow_symlink_to_file, tc) 133 { 134 atf_tc_set_md_var(tc, "descr", 135 "FTS_FOLLOW on FTS_SL to regular file yields FTS_F"); 136 } 137 ATF_TC_BODY(follow_symlink_to_file, tc) 138 { 139 char *paths[] = { "dir", NULL }; 140 FTS *fts; 141 FTSENT *ent; 142 bool followed; 143 144 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 145 ATF_REQUIRE_EQ(0, close(creat("dir/target", 0644))); 146 ATF_REQUIRE_EQ(0, symlink("target", "dir/link")); 147 148 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 149 150 followed = false; 151 while ((ent = fts_read(fts)) != NULL) { 152 if (ent->fts_info == FTS_SL && 153 strcmp(ent->fts_name, "link") == 0) 154 ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_FOLLOW)); 155 else if (ent->fts_info == FTS_F && 156 strcmp(ent->fts_name, "link") == 0) 157 followed = true; 158 } 159 ATF_CHECK_MSG(followed, 160 "FTS_FOLLOW on symlink-to-file must yield FTS_F"); 161 162 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 163 } 164 165 /* 166 * FTS_FOLLOW on an FTS_SL entry pointing to a directory causes descent 167 * into the target directory. 168 */ 169 ATF_TC(follow_symlink_to_dir); 170 ATF_TC_HEAD(follow_symlink_to_dir, tc) 171 { 172 atf_tc_set_md_var(tc, "descr", 173 "FTS_FOLLOW on FTS_SL to directory causes descent"); 174 } 175 ATF_TC_BODY(follow_symlink_to_dir, tc) 176 { 177 char *paths[] = { "dir", NULL }; 178 FTS *fts; 179 FTSENT *ent; 180 bool saw_inside; 181 182 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 183 ATF_REQUIRE_EQ(0, mkdir("dir/real", 0755)); 184 ATF_REQUIRE_EQ(0, close(creat("dir/real/inside", 0644))); 185 ATF_REQUIRE_EQ(0, symlink("real", "dir/link")); 186 187 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 188 189 saw_inside = false; 190 while ((ent = fts_read(fts)) != NULL) { 191 if (ent->fts_info == FTS_SL && 192 strcmp(ent->fts_name, "link") == 0) 193 ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_FOLLOW)); 194 if (ent->fts_info == FTS_F && 195 strcmp(ent->fts_name, "inside") == 0 && 196 strcmp(ent->fts_path, "dir/link/inside") == 0) 197 saw_inside = true; 198 } 199 ATF_CHECK_MSG(saw_inside, 200 "FTS_FOLLOW on symlink-to-dir should descend and visit 'inside'"); 201 202 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 203 } 204 205 /* 206 * FTS_FOLLOW on a dangling symlink (FTS_SLNONE) yields FTS_SLNONE again. 207 * FTS_SLNONE requires FTS_LOGICAL — under FTS_PHYSICAL a dangling 208 * symlink is reported as FTS_SL. 209 */ 210 ATF_TC(follow_dead_symlink); 211 ATF_TC_HEAD(follow_dead_symlink, tc) 212 { 213 atf_tc_set_md_var(tc, "descr", 214 "FTS_FOLLOW on dead symlink yields FTS_SLNONE"); 215 } 216 ATF_TC_BODY(follow_dead_symlink, tc) 217 { 218 char *paths[] = { "dead", NULL }; 219 FTS *fts; 220 FTSENT *ent; 221 222 ATF_REQUIRE_EQ(0, symlink("no-such-target", "dead")); 223 224 ATF_REQUIRE((fts = fts_open(paths, FTS_LOGICAL, NULL)) != NULL); 225 226 ent = fts_read(fts); 227 ATF_REQUIRE(ent != NULL); 228 ATF_REQUIRE_EQ_MSG(FTS_SLNONE, ent->fts_info, 229 "expected FTS_SLNONE for dead symlink, got %d", ent->fts_info); 230 231 ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_FOLLOW)); 232 ent = fts_read(fts); 233 ATF_REQUIRE(ent != NULL); 234 ATF_CHECK_EQ_MSG(FTS_SLNONE, ent->fts_info, 235 "FTS_FOLLOW on dead symlink should still be FTS_SLNONE, got %d", 236 ent->fts_info); 237 238 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 239 } 240 241 /* 242 * FTS_SKIP on an FTS_D node prevents descent into that directory. 243 * The next fts_read() converts the node to FTS_DP without visiting 244 * any children. 245 */ 246 ATF_TC(skip); 247 ATF_TC_HEAD(skip, tc) 248 { 249 atf_tc_set_md_var(tc, "descr", 250 "FTS_SKIP prevents descent into a directory"); 251 } 252 ATF_TC_BODY(skip, tc) 253 { 254 char *paths[] = { "dir", NULL }; 255 FTS *fts; 256 FTSENT *ent; 257 bool saw_inside; 258 259 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 260 ATF_REQUIRE_EQ(0, mkdir("dir/skip_me", 0755)); 261 ATF_REQUIRE_EQ(0, close(creat("dir/skip_me/inside", 0644))); 262 ATF_REQUIRE_EQ(0, close(creat("dir/sibling", 0644))); 263 264 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 265 266 saw_inside = false; 267 while ((ent = fts_read(fts)) != NULL) { 268 if (ent->fts_info == FTS_D && 269 strcmp(ent->fts_name, "skip_me") == 0) 270 ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_SKIP)); 271 if (strcmp(ent->fts_name, "inside") == 0) 272 saw_inside = true; 273 } 274 ATF_CHECK_MSG(!saw_inside, 275 "FTS_SKIP: 'inside' must not have been visited"); 276 277 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 278 } 279 280 /* 281 * fts_set_clientptr() and fts_get_clientptr() store and retrieve an 282 * arbitrary pointer on the FTS stream. 283 */ 284 ATF_TC(clientptr_roundtrip); 285 ATF_TC_HEAD(clientptr_roundtrip, tc) 286 { 287 atf_tc_set_md_var(tc, "descr", 288 "fts_set_clientptr / fts_get_clientptr round-trip"); 289 } 290 ATF_TC_BODY(clientptr_roundtrip, tc) 291 { 292 char *paths[] = { "dir", NULL }; 293 FTS *fts; 294 FTSENT *ent; 295 int value = 42; 296 297 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 298 ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); 299 300 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 301 302 /* Initially NULL. */ 303 ATF_CHECK_EQ(NULL, fts_get_clientptr(fts)); 304 305 fts_set_clientptr(fts, &value); 306 307 while ((ent = fts_read(fts)) != NULL) { 308 /* 309 * Verify the pointer is accessible and correct 310 * while traversal is active. 311 */ 312 ATF_CHECK_EQ_MSG(&value, fts_get_clientptr(fts), 313 "fts_get_clientptr did not return the stored pointer " 314 "for entry '%s'", ent->fts_name); 315 } 316 317 /* Overwrite with NULL, verify. */ 318 fts_set_clientptr(fts, NULL); 319 ATF_CHECK_EQ(NULL, fts_get_clientptr(fts)); 320 321 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 322 } 323 324 /* 325 * fts_get_stream() returns the parent FTS* from any FTSENT* returned 326 * by fts_read(). 327 */ 328 ATF_TC(get_stream_backpointer); 329 ATF_TC_HEAD(get_stream_backpointer, tc) 330 { 331 atf_tc_set_md_var(tc, "descr", 332 "fts_get_stream returns the parent FTS* from an FTSENT*"); 333 } 334 ATF_TC_BODY(get_stream_backpointer, tc) 335 { 336 char *paths[] = { "dir", NULL }; 337 FTS *fts; 338 FTSENT *ent; 339 340 ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); 341 ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); 342 343 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); 344 345 while ((ent = fts_read(fts)) != NULL) { 346 ATF_CHECK_EQ_MSG(fts, fts_get_stream(ent), 347 "fts_get_stream(ent) must return the parent FTS*, " 348 "entry: %s info: %d", 349 ent->fts_name, ent->fts_info); 350 } 351 352 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); 353 } 354 355 ATF_TP_ADD_TCS(tp) 356 { 357 ATF_TP_ADD_TC(tp, invalid_options); 358 ATF_TP_ADD_TC(tp, again); 359 ATF_TP_ADD_TC(tp, again_consecutive); 360 ATF_TP_ADD_TC(tp, follow_symlink_to_file); 361 ATF_TP_ADD_TC(tp, follow_symlink_to_dir); 362 ATF_TP_ADD_TC(tp, follow_dead_symlink); 363 ATF_TP_ADD_TC(tp, skip); 364 ATF_TP_ADD_TC(tp, clientptr_roundtrip); 365 ATF_TP_ADD_TC(tp, get_stream_backpointer); 366 367 return (atf_no_error()); 368 } 369