1 /*-
2 * Copyright (c) 2025 Klara, Inc.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7 #include <sys/stat.h>
8
9 #include <fcntl.h>
10 #include <fts.h>
11 #include <stdbool.h>
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15
16 #include <atf-c.h>
17
18 struct fts_expect {
19 int fts_info;
20 const char *fts_name;
21 const char *fts_accpath;
22 };
23
24 struct fts_testcase {
25 char **paths;
26 int fts_options;
27 struct fts_expect *fts_expect;
28 };
29
30 static char *all_paths[] = {
31 "dir",
32 "dirl",
33 "file",
34 "filel",
35 "dead",
36 "noent",
37 NULL
38 };
39
40 /* shorter name for dead links */
41 #define FTS_DL FTS_SLNONE
42
43 /* are we being debugged? */
44 static bool debug;
45
46 /*
47 * Prepare the files and directories we will be inspecting.
48 */
49 static void
fts_options_prepare(const struct atf_tc * tc)50 fts_options_prepare(const struct atf_tc *tc)
51 {
52 debug = !getenv("__RUNNING_INSIDE_ATF_RUN") &&
53 isatty(STDERR_FILENO);
54 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
55 ATF_REQUIRE_EQ(0, close(creat("file", 0644)));
56 ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
57 ATF_REQUIRE_EQ(0, symlink("..", "dir/up"));
58 ATF_REQUIRE_EQ(0, symlink("dir", "dirl"));
59 ATF_REQUIRE_EQ(0, symlink("file", "filel"));
60 ATF_REQUIRE_EQ(0, symlink("noent", "dead"));
61 }
62
63 /*
64 * Lexical order for reproducability.
65 */
66 static int
fts_options_compar(const FTSENT * const * a,const FTSENT * const * b)67 fts_options_compar(const FTSENT * const *a, const FTSENT * const *b)
68 {
69 return (strcmp((*a)->fts_name, (*b)->fts_name));
70 }
71
72 /*
73 * Run FTS with the specified paths and options and verify that it
74 * produces the expected result in the correct order.
75 */
76 static void
fts_options_test(const struct atf_tc * tc,const struct fts_testcase * fts_tc)77 fts_options_test(const struct atf_tc *tc, const struct fts_testcase *fts_tc)
78 {
79 FTS *fts;
80 FTSENT *ftse;
81 const struct fts_expect *expect = fts_tc->fts_expect;
82 long level = 0;
83
84 fts = fts_open(fts_tc->paths, fts_tc->fts_options, fts_options_compar);
85 ATF_REQUIRE_MSG(fts != NULL, "fts_open(): %m");
86 while ((ftse = fts_read(fts)) != NULL && expect->fts_name != NULL) {
87 if (expect->fts_info == FTS_DP)
88 level--;
89 if (debug) {
90 fprintf(stderr, "%2ld %2d %s\n", level,
91 ftse->fts_info, ftse->fts_name);
92 }
93 ATF_CHECK_STREQ(expect->fts_name, ftse->fts_name);
94 ATF_CHECK_STREQ(expect->fts_accpath, ftse->fts_accpath);
95 ATF_CHECK_INTEQ(expect->fts_info, ftse->fts_info);
96 ATF_CHECK_INTEQ(level, ftse->fts_level);
97 if (expect->fts_info == FTS_D)
98 level++;
99 expect++;
100 }
101 ATF_CHECK_EQ(NULL, ftse);
102 ATF_CHECK_EQ(NULL, expect->fts_name);
103 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
104 }
105
106 ATF_TC(fts_options_logical);
ATF_TC_HEAD(fts_options_logical,tc)107 ATF_TC_HEAD(fts_options_logical, tc)
108 {
109 atf_tc_set_md_var(tc, "descr", "FTS_LOGICAL");
110 }
ATF_TC_BODY(fts_options_logical,tc)111 ATF_TC_BODY(fts_options_logical, tc)
112 {
113 fts_options_prepare(tc);
114 fts_options_test(tc, &(struct fts_testcase){
115 all_paths,
116 FTS_LOGICAL,
117 (struct fts_expect[]){
118 { FTS_DL, "dead", "dead" },
119 { FTS_D, "dir", "dir" },
120 { FTS_F, "file", "dir/file" },
121 { FTS_D, "up", "dir/up" },
122 { FTS_DL, "dead", "dir/up/dead" },
123 { FTS_DC, "dir", "dir/up/dir" },
124 { FTS_DC, "dirl", "dir/up/dirl" },
125 { FTS_F, "file", "dir/up/file" },
126 { FTS_F, "filel", "dir/up/filel" },
127 { FTS_DP, "up", "dir/up" },
128 { FTS_DP, "dir", "dir" },
129 { FTS_D, "dirl", "dirl" },
130 { FTS_F, "file", "dirl/file" },
131 { FTS_D, "up", "dirl/up" },
132 { FTS_DL, "dead", "dirl/up/dead" },
133 { FTS_DC, "dir", "dirl/up/dir" },
134 { FTS_DC, "dirl", "dirl/up/dirl" },
135 { FTS_F, "file", "dirl/up/file" },
136 { FTS_F, "filel", "dirl/up/filel" },
137 { FTS_DP, "up", "dirl/up" },
138 { FTS_DP, "dirl", "dirl" },
139 { FTS_F, "file", "file" },
140 { FTS_F, "filel", "filel" },
141 { FTS_NS, "noent", "noent" },
142 { 0 }
143 },
144 });
145 }
146
147 ATF_TC(fts_options_logical_nostat);
ATF_TC_HEAD(fts_options_logical_nostat,tc)148 ATF_TC_HEAD(fts_options_logical_nostat, tc)
149 {
150 atf_tc_set_md_var(tc, "descr", "FTS_LOGICAL | FTS_NOSTAT");
151 }
ATF_TC_BODY(fts_options_logical_nostat,tc)152 ATF_TC_BODY(fts_options_logical_nostat, tc)
153 {
154 /*
155 * While FTS_LOGICAL is not documented as being incompatible with
156 * FTS_NOSTAT, and FTS does not clear FTS_NOSTAT if FTS_LOGICAL is
157 * set, FTS_LOGICAL effectively nullifies FTS_NOSTAT by overriding
158 * the follow check in fts_stat(). In theory, FTS could easily be
159 * changed to only stat links (to check what they point to) in the
160 * FTS_LOGICAL | FTS_NOSTAT case, which would produce a different
161 * result here, so keep the test around in case that ever happens.
162 */
163 atf_tc_expect_fail("FTS_LOGICAL nullifies FTS_NOSTAT");
164 fts_options_prepare(tc);
165 fts_options_test(tc, &(struct fts_testcase){
166 all_paths,
167 FTS_LOGICAL | FTS_NOSTAT,
168 (struct fts_expect[]){
169 { FTS_DL, "dead", "dead" },
170 { FTS_D, "dir", "dir" },
171 { FTS_NSOK, "file", "dir/file" },
172 { FTS_D, "up", "dir/up" },
173 { FTS_DL, "dead", "dir/up/dead" },
174 { FTS_DC, "dir", "dir/up/dir" },
175 { FTS_DC, "dirl", "dir/up/dirl" },
176 { FTS_NSOK, "file", "dir/up/file" },
177 { FTS_NSOK, "filel", "dir/up/filel" },
178 { FTS_DP, "up", "dir/up" },
179 { FTS_DP, "dir", "dir" },
180 { FTS_D, "dirl", "dirl" },
181 { FTS_NSOK, "file", "dirl/file" },
182 { FTS_D, "up", "dirl/up" },
183 { FTS_DL, "dead", "dirl/up/dead" },
184 { FTS_DC, "dir", "dirl/up/dir" },
185 { FTS_DC, "dirl", "dirl/up/dirl" },
186 { FTS_NSOK, "file", "dirl/up/file" },
187 { FTS_NSOK, "filel", "dirl/up/filel" },
188 { FTS_DP, "up", "dirl/up" },
189 { FTS_DP, "dirl", "dirl" },
190 { FTS_F, "file", "file" },
191 { FTS_F, "filel", "filel" },
192 { FTS_NS, "noent", "noent" },
193 { 0 }
194 },
195 });
196 }
197
198 ATF_TC(fts_options_logical_seedot);
ATF_TC_HEAD(fts_options_logical_seedot,tc)199 ATF_TC_HEAD(fts_options_logical_seedot, tc)
200 {
201 atf_tc_set_md_var(tc, "descr", "FTS_LOGICAL | FTS_SEEDOT");
202 }
ATF_TC_BODY(fts_options_logical_seedot,tc)203 ATF_TC_BODY(fts_options_logical_seedot, tc)
204 {
205 fts_options_prepare(tc);
206 fts_options_test(tc, &(struct fts_testcase){
207 all_paths,
208 FTS_LOGICAL | FTS_SEEDOT,
209 (struct fts_expect[]){
210 { FTS_DL, "dead", "dead" },
211 { FTS_D, "dir", "dir" },
212 { FTS_DOT, ".", "dir/." },
213 { FTS_DOT, "..", "dir/.." },
214 { FTS_F, "file", "dir/file" },
215 { FTS_D, "up", "dir/up" },
216 { FTS_DOT, ".", "dir/up/." },
217 { FTS_DOT, "..", "dir/up/.." },
218 { FTS_DL, "dead", "dir/up/dead" },
219 { FTS_DC, "dir", "dir/up/dir" },
220 { FTS_DC, "dirl", "dir/up/dirl" },
221 { FTS_F, "file", "dir/up/file" },
222 { FTS_F, "filel", "dir/up/filel" },
223 { FTS_DP, "up", "dir/up" },
224 { FTS_DP, "dir", "dir" },
225 { FTS_D, "dirl", "dirl" },
226 { FTS_DOT, ".", "dirl/." },
227 { FTS_DOT, "..", "dirl/.." },
228 { FTS_F, "file", "dirl/file" },
229 { FTS_D, "up", "dirl/up" },
230 { FTS_DOT, ".", "dirl/up/." },
231 { FTS_DOT, "..", "dirl/up/.." },
232 { FTS_DL, "dead", "dirl/up/dead" },
233 { FTS_DC, "dir", "dirl/up/dir" },
234 { FTS_DC, "dirl", "dirl/up/dirl" },
235 { FTS_F, "file", "dirl/up/file" },
236 { FTS_F, "filel", "dirl/up/filel" },
237 { FTS_DP, "up", "dirl/up" },
238 { FTS_DP, "dirl", "dirl" },
239 { FTS_F, "file", "file" },
240 { FTS_F, "filel", "filel" },
241 { FTS_NS, "noent", "noent" },
242 { 0 }
243 },
244 });
245 }
246
247 ATF_TC(fts_options_physical);
ATF_TC_HEAD(fts_options_physical,tc)248 ATF_TC_HEAD(fts_options_physical, tc)
249 {
250 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL");
251 }
ATF_TC_BODY(fts_options_physical,tc)252 ATF_TC_BODY(fts_options_physical, tc)
253 {
254 fts_options_prepare(tc);
255 fts_options_test(tc, &(struct fts_testcase){
256 all_paths,
257 FTS_PHYSICAL,
258 (struct fts_expect[]){
259 { FTS_SL, "dead", "dead" },
260 { FTS_D, "dir", "dir" },
261 { FTS_F, "file", "file" },
262 { FTS_SL, "up", "up" },
263 { FTS_DP, "dir", "dir" },
264 { FTS_SL, "dirl", "dirl" },
265 { FTS_F, "file", "file" },
266 { FTS_SL, "filel", "filel" },
267 { FTS_NS, "noent", "noent" },
268 { 0 }
269 },
270 });
271 }
272
273 ATF_TC(fts_options_physical_nochdir);
ATF_TC_HEAD(fts_options_physical_nochdir,tc)274 ATF_TC_HEAD(fts_options_physical_nochdir, tc)
275 {
276 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL | FTS_NOCHDIR");
277 }
ATF_TC_BODY(fts_options_physical_nochdir,tc)278 ATF_TC_BODY(fts_options_physical_nochdir, tc)
279 {
280 fts_options_prepare(tc);
281 fts_options_test(tc, &(struct fts_testcase){
282 all_paths,
283 FTS_PHYSICAL | FTS_NOCHDIR,
284 (struct fts_expect[]){
285 { FTS_SL, "dead", "dead" },
286 { FTS_D, "dir", "dir" },
287 { FTS_F, "file", "dir/file" },
288 { FTS_SL, "up", "dir/up" },
289 { FTS_DP, "dir", "dir" },
290 { FTS_SL, "dirl", "dirl" },
291 { FTS_F, "file", "file" },
292 { FTS_SL, "filel", "filel" },
293 { FTS_NS, "noent", "noent" },
294 { 0 }
295 },
296 });
297 }
298
299 ATF_TC(fts_options_physical_comfollow);
ATF_TC_HEAD(fts_options_physical_comfollow,tc)300 ATF_TC_HEAD(fts_options_physical_comfollow, tc)
301 {
302 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL | FTS_COMFOLLOW");
303 }
ATF_TC_BODY(fts_options_physical_comfollow,tc)304 ATF_TC_BODY(fts_options_physical_comfollow, tc)
305 {
306 fts_options_prepare(tc);
307 fts_options_test(tc, &(struct fts_testcase){
308 all_paths,
309 FTS_PHYSICAL | FTS_COMFOLLOW,
310 (struct fts_expect[]){
311 { FTS_DL, "dead", "dead" },
312 { FTS_D, "dir", "dir" },
313 { FTS_F, "file", "file" },
314 { FTS_SL, "up", "up" },
315 { FTS_DP, "dir", "dir" },
316 { FTS_D, "dirl", "dirl" },
317 { FTS_F, "file", "file" },
318 { FTS_SL, "up", "up" },
319 { FTS_DP, "dirl", "dirl" },
320 { FTS_F, "file", "file" },
321 { FTS_F, "filel", "filel" },
322 { FTS_NS, "noent", "noent" },
323 { 0 }
324 },
325 });
326 }
327
328 ATF_TC(fts_options_physical_comfollowdir);
ATF_TC_HEAD(fts_options_physical_comfollowdir,tc)329 ATF_TC_HEAD(fts_options_physical_comfollowdir, tc)
330 {
331 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL | FTS_COMFOLLOWDIR");
332 }
ATF_TC_BODY(fts_options_physical_comfollowdir,tc)333 ATF_TC_BODY(fts_options_physical_comfollowdir, tc)
334 {
335 fts_options_prepare(tc);
336 fts_options_test(tc, &(struct fts_testcase){
337 all_paths,
338 FTS_PHYSICAL | FTS_COMFOLLOWDIR,
339 (struct fts_expect[]){
340 { FTS_DL, "dead", "dead" },
341 { FTS_D, "dir", "dir" },
342 { FTS_F, "file", "file" },
343 { FTS_SL, "up", "up" },
344 { FTS_DP, "dir", "dir" },
345 { FTS_D, "dirl", "dirl" },
346 { FTS_F, "file", "file" },
347 { FTS_SL, "up", "up" },
348 { FTS_DP, "dirl", "dirl" },
349 { FTS_F, "file", "file" },
350 { FTS_SL, "filel", "filel" },
351 { FTS_NS, "noent", "noent" },
352 { 0 }
353 },
354 });
355 }
356
357 ATF_TC(fts_options_physical_nostat);
ATF_TC_HEAD(fts_options_physical_nostat,tc)358 ATF_TC_HEAD(fts_options_physical_nostat, tc)
359 {
360 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL | FTS_NOSTAT");
361 }
ATF_TC_BODY(fts_options_physical_nostat,tc)362 ATF_TC_BODY(fts_options_physical_nostat, tc)
363 {
364 fts_options_prepare(tc);
365 fts_options_test(tc, &(struct fts_testcase){
366 all_paths,
367 FTS_PHYSICAL | FTS_NOSTAT,
368 (struct fts_expect[]){
369 { FTS_SL, "dead", "dead" },
370 { FTS_D, "dir", "dir" },
371 { FTS_NSOK, "file", "file" },
372 { FTS_NSOK, "up", "up" },
373 { FTS_DP, "dir", "dir" },
374 { FTS_SL, "dirl", "dirl" },
375 { FTS_F, "file", "file" },
376 { FTS_SL, "filel", "filel" },
377 { FTS_NS, "noent", "noent" },
378 { 0 }
379 },
380 });
381 }
382
383 ATF_TC(fts_options_physical_nostat_type);
ATF_TC_HEAD(fts_options_physical_nostat_type,tc)384 ATF_TC_HEAD(fts_options_physical_nostat_type, tc)
385 {
386 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL | FTS_NOSTAT_TYPE");
387 }
ATF_TC_BODY(fts_options_physical_nostat_type,tc)388 ATF_TC_BODY(fts_options_physical_nostat_type, tc)
389 {
390 fts_options_prepare(tc);
391 fts_options_test(tc, &(struct fts_testcase){
392 all_paths,
393 FTS_PHYSICAL | FTS_NOSTAT_TYPE,
394 (struct fts_expect[]){
395 { FTS_SL, "dead", "dead" },
396 { FTS_D, "dir", "dir" },
397 { FTS_F, "file", "file" },
398 { FTS_SL, "up", "up" },
399 { FTS_DP, "dir", "dir" },
400 { FTS_SL, "dirl", "dirl" },
401 { FTS_F, "file", "file" },
402 { FTS_SL, "filel", "filel" },
403 { FTS_NS, "noent", "noent" },
404 { 0 }
405 },
406 });
407 }
408
409 ATF_TC(fts_options_physical_seedot);
ATF_TC_HEAD(fts_options_physical_seedot,tc)410 ATF_TC_HEAD(fts_options_physical_seedot, tc)
411 {
412 atf_tc_set_md_var(tc, "descr", "FTS_PHYSICAL | FTS_SEEDOT");
413 }
ATF_TC_BODY(fts_options_physical_seedot,tc)414 ATF_TC_BODY(fts_options_physical_seedot, tc)
415 {
416 fts_options_prepare(tc);
417 fts_options_test(tc, &(struct fts_testcase){
418 all_paths,
419 FTS_PHYSICAL | FTS_SEEDOT,
420 (struct fts_expect[]){
421 { FTS_SL, "dead", "dead" },
422 { FTS_D, "dir", "dir" },
423 { FTS_DOT, ".", "." },
424 { FTS_DOT, "..", ".." },
425 { FTS_F, "file", "file" },
426 { FTS_SL, "up", "up" },
427 { FTS_DP, "dir", "dir" },
428 { FTS_SL, "dirl", "dirl" },
429 { FTS_F, "file", "file" },
430 { FTS_SL, "filel", "filel" },
431 { FTS_NS, "noent", "noent" },
432 { 0 }
433 },
434 });
435 }
436
437 /*
438 * TODO: Add tests for FTS_XDEV and FTS_WHITEOUT
439 */
440
ATF_TP_ADD_TCS(tp)441 ATF_TP_ADD_TCS(tp)
442 {
443 ATF_TP_ADD_TC(tp, fts_options_logical);
444 ATF_TP_ADD_TC(tp, fts_options_logical_nostat);
445 ATF_TP_ADD_TC(tp, fts_options_logical_seedot);
446 ATF_TP_ADD_TC(tp, fts_options_physical);
447 ATF_TP_ADD_TC(tp, fts_options_physical_nochdir);
448 ATF_TP_ADD_TC(tp, fts_options_physical_comfollow);
449 ATF_TP_ADD_TC(tp, fts_options_physical_comfollowdir);
450 ATF_TP_ADD_TC(tp, fts_options_physical_nostat);
451 ATF_TP_ADD_TC(tp, fts_options_physical_nostat_type);
452 ATF_TP_ADD_TC(tp, fts_options_physical_seedot);
453 return (atf_no_error());
454 }
455