1 /*
2 * Copyright (c) 2026 Jitendra Bhati
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7 /*
8 * Tests for fts_children().
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 <string.h>
20 #include <unistd.h>
21
22 #include <atf-c.h>
23
24 #include "fts_test.h"
25
26 /*
27 * fts_children() before fts_read() returns the list of root entries.
28 */
29 ATF_TC(before_read);
ATF_TC_HEAD(before_read,tc)30 ATF_TC_HEAD(before_read, tc)
31 {
32 atf_tc_set_md_var(tc, "descr",
33 "fts_children before fts_read returns root entry list");
34 }
ATF_TC_BODY(before_read,tc)35 ATF_TC_BODY(before_read, tc)
36 {
37 char *paths[] = { "dir", NULL };
38 FTS *fts;
39 FTSENT *children, *p;
40 int count;
41
42 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
43 ATF_REQUIRE_EQ(0, close(creat("dir/a", 0644)));
44
45 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
46
47 errno = 0;
48 children = fts_children(fts, 0);
49 ATF_REQUIRE_MSG(children != NULL,
50 "fts_children before fts_read must return the root list");
51 ATF_CHECK_EQ(0, errno);
52
53 count = 0;
54 for (p = children; p != NULL; p = p->fts_link) {
55 ATF_CHECK_EQ_MSG(FTS_D, p->fts_info,
56 "root entry should be FTS_D, got %d", p->fts_info);
57 count++;
58 }
59 ATF_CHECK_EQ_MSG(1, count,
60 "expected 1 root entry, found %d", count);
61
62 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
63 }
64
65 /*
66 * fts_children() on an empty directory returns NULL with errno == 0.
67 * errno=0 distinguishes "empty" from an actual error.
68 */
69 ATF_TC(empty_dir);
ATF_TC_HEAD(empty_dir,tc)70 ATF_TC_HEAD(empty_dir, tc)
71 {
72 atf_tc_set_md_var(tc, "descr",
73 "fts_children on empty directory returns NULL with errno 0");
74 }
ATF_TC_BODY(empty_dir,tc)75 ATF_TC_BODY(empty_dir, tc)
76 {
77 char *paths[] = { "dir", NULL };
78 FTS *fts;
79 FTSENT *ent, *children;
80
81 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
82
83 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
84
85 ent = fts_read(fts);
86 ATF_REQUIRE(ent != NULL);
87 ATF_REQUIRE_EQ_MSG(FTS_D, ent->fts_info,
88 "expected FTS_D, got %d", ent->fts_info);
89
90 errno = 1; /* sentinel — fts_children must clear this */
91 children = fts_children(fts, 0);
92 ATF_CHECK_MSG(children == NULL,
93 "fts_children on empty dir must return NULL");
94 ATF_CHECK_EQ_MSG(0, errno,
95 "fts_children on empty dir must set errno=0, got %d", errno);
96
97 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
98 }
99
100 /*
101 * fts_children() on a non-empty directory returns a linked list of all
102 * children in comparator order.
103 */
104 ATF_TC(nonempty_dir);
ATF_TC_HEAD(nonempty_dir,tc)105 ATF_TC_HEAD(nonempty_dir, tc)
106 {
107 atf_tc_set_md_var(tc, "descr",
108 "fts_children on non-empty directory returns all children");
109 }
ATF_TC_BODY(nonempty_dir,tc)110 ATF_TC_BODY(nonempty_dir, tc)
111 {
112 static const char *expected[] = { "a", "b", "c", NULL };
113 char *paths[] = { "dir", NULL };
114 FTS *fts;
115 FTSENT *ent, *children, *p;
116 int i;
117
118 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
119 ATF_REQUIRE_EQ(0, close(creat("dir/a", 0644)));
120 ATF_REQUIRE_EQ(0, close(creat("dir/b", 0644)));
121 ATF_REQUIRE_EQ(0, close(creat("dir/c", 0644)));
122
123 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
124 fts_lexical_compar)) != NULL);
125
126 ent = fts_read(fts);
127 ATF_REQUIRE(ent != NULL);
128 ATF_REQUIRE_EQ(FTS_D, ent->fts_info);
129
130 children = fts_children(fts, 0);
131 ATF_REQUIRE_MSG(children != NULL, "fts_children(): %m");
132
133 i = 0;
134 for (p = children; p != NULL; p = p->fts_link, i++) {
135 ATF_REQUIRE_MSG(expected[i] != NULL,
136 "more children returned than expected");
137 ATF_CHECK_STREQ(expected[i], p->fts_name);
138 ATF_CHECK_EQ(FTS_F, p->fts_info);
139 }
140 ATF_CHECK_MSG(expected[i] == NULL,
141 "fewer children returned than expected");
142
143 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
144 }
145
146 /*
147 * fts_children() called twice on the same FTS_D node must return an
148 * equivalent list both times.
149 */
150 ATF_TC(called_twice);
ATF_TC_HEAD(called_twice,tc)151 ATF_TC_HEAD(called_twice, tc)
152 {
153 atf_tc_set_md_var(tc, "descr",
154 "fts_children called twice returns equivalent results");
155 }
ATF_TC_BODY(called_twice,tc)156 ATF_TC_BODY(called_twice, tc)
157 {
158 char *paths[] = { "dir", NULL };
159 FTS *fts;
160 FTSENT *ent, *first, *second, *p;
161 int count1, count2;
162
163 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
164 ATF_REQUIRE_EQ(0, close(creat("dir/x", 0644)));
165 ATF_REQUIRE_EQ(0, close(creat("dir/y", 0644)));
166
167 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
168 fts_lexical_compar)) != NULL);
169
170 ent = fts_read(fts);
171 ATF_REQUIRE(ent != NULL);
172 ATF_REQUIRE_EQ(FTS_D, ent->fts_info);
173
174 first = fts_children(fts, 0);
175 ATF_REQUIRE_MSG(first != NULL, "first fts_children call: %m");
176
177 count1 = 0;
178 for (p = first; p != NULL; p = p->fts_link)
179 count1++;
180
181 /*
182 * The second call frees the first list and rebuilds. Do not
183 * dereference 'first' after this point — it has been freed.
184 */
185 second = fts_children(fts, 0);
186 ATF_REQUIRE_MSG(second != NULL, "second fts_children call: %m");
187
188 count2 = 0;
189 for (p = second; p != NULL; p = p->fts_link)
190 count2++;
191
192 ATF_CHECK_EQ_MSG(count1, count2,
193 "first call returned %d children, second returned %d",
194 count1, count2);
195 ATF_CHECK_EQ(2, count2);
196
197 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
198 }
199
200 /*
201 * fts_children(FTS_NAMEONLY): only fts_name and fts_namelen are filled.
202 * fts_info is FTS_NSOK for every entry.
203 */
204 ATF_TC(nameonly);
ATF_TC_HEAD(nameonly,tc)205 ATF_TC_HEAD(nameonly, tc)
206 {
207 atf_tc_set_md_var(tc, "descr",
208 "FTS_NAMEONLY fills only fts_name, fts_info is FTS_NSOK");
209 }
ATF_TC_BODY(nameonly,tc)210 ATF_TC_BODY(nameonly, tc)
211 {
212 char *paths[] = { "dir", NULL };
213 FTS *fts;
214 FTSENT *ent, *children, *p;
215 int count;
216
217 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
218 ATF_REQUIRE_EQ(0, close(creat("dir/f1", 0644)));
219 ATF_REQUIRE_EQ(0, close(creat("dir/f2", 0644)));
220
221 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
222 fts_lexical_compar)) != NULL);
223
224 ent = fts_read(fts);
225 ATF_REQUIRE(ent != NULL);
226 ATF_REQUIRE_EQ(FTS_D, ent->fts_info);
227
228 children = fts_children(fts, FTS_NAMEONLY);
229 ATF_REQUIRE_MSG(children != NULL, "fts_children(FTS_NAMEONLY): %m");
230
231 count = 0;
232 for (p = children; p != NULL; p = p->fts_link) {
233 ATF_CHECK_MSG(p->fts_name[0] != '\0',
234 "FTS_NAMEONLY: fts_name is empty");
235 ATF_CHECK_EQ(strlen(p->fts_name), p->fts_namelen);
236 ATF_CHECK_EQ_MSG(FTS_NSOK, p->fts_info,
237 "FTS_NAMEONLY: expected FTS_NSOK, got %d", p->fts_info);
238 count++;
239 }
240 ATF_CHECK_EQ(2, count);
241
242 /* Normal traversal must still work after FTS_NAMEONLY. */
243 while (fts_read(fts) != NULL)
244 ;
245 ATF_CHECK_EQ_MSG(0, errno,
246 "traversal after FTS_NAMEONLY ended with errno %d", errno);
247
248 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
249 }
250
251 /*
252 * fts_children() on a non-directory node must return NULL with errno == 0.
253 */
254 ATF_TC(nondirectory);
ATF_TC_HEAD(nondirectory,tc)255 ATF_TC_HEAD(nondirectory, tc)
256 {
257 atf_tc_set_md_var(tc, "descr",
258 "fts_children on a non-directory node returns NULL with errno 0");
259 }
ATF_TC_BODY(nondirectory,tc)260 ATF_TC_BODY(nondirectory, tc)
261 {
262 char *paths[] = { "dir", NULL };
263 FTS *fts;
264 FTSENT *ent;
265
266 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
267 ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
268
269 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
270 fts_lexical_compar)) != NULL);
271
272 ent = fts_read(fts); /* FTS_D dir */
273 ATF_REQUIRE(ent != NULL);
274 ATF_REQUIRE_EQ(FTS_D, ent->fts_info);
275
276 ent = fts_read(fts); /* FTS_F file */
277 ATF_REQUIRE(ent != NULL);
278 ATF_REQUIRE_EQ(FTS_F, ent->fts_info);
279
280 errno = 1;
281 ATF_CHECK_MSG(fts_children(fts, 0) == NULL,
282 "fts_children on FTS_F must return NULL");
283 ATF_CHECK_EQ_MSG(0, errno,
284 "fts_children on FTS_F must set errno=0, got %d", errno);
285
286 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
287 }
288
289 /*
290 * fts_children() with an invalid options value must return NULL with
291 * errno == EINVAL.
292 */
293 ATF_TC(invalid_options);
ATF_TC_HEAD(invalid_options,tc)294 ATF_TC_HEAD(invalid_options, tc)
295 {
296 atf_tc_set_md_var(tc, "descr",
297 "fts_children with invalid options returns NULL with EINVAL");
298 }
ATF_TC_BODY(invalid_options,tc)299 ATF_TC_BODY(invalid_options, tc)
300 {
301 char *paths[] = { ".", NULL };
302 FTS *fts;
303
304 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
305
306 ATF_REQUIRE_ERRNO(EINVAL, fts_children(fts, 99) == NULL);
307
308 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
309 }
310
ATF_TP_ADD_TCS(tp)311 ATF_TP_ADD_TCS(tp)
312 {
313 fts_check_debug();
314 ATF_TP_ADD_TC(tp, before_read);
315 ATF_TP_ADD_TC(tp, empty_dir);
316 ATF_TP_ADD_TC(tp, nonempty_dir);
317 ATF_TP_ADD_TC(tp, called_twice);
318 ATF_TP_ADD_TC(tp, nameonly);
319 ATF_TP_ADD_TC(tp, nondirectory);
320 ATF_TP_ADD_TC(tp, invalid_options);
321
322 return (atf_no_error());
323 }
324