xref: /freebsd/lib/libc/tests/gen/fts_set_test.c (revision ee213339f4b21782cd1b44086ff9b7fe1fd682c5)
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);
ATF_TC_HEAD(invalid_options,tc)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 }
ATF_TC_BODY(invalid_options,tc)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);
ATF_TC_HEAD(again,tc)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 }
ATF_TC_BODY(again,tc)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);
ATF_TC_HEAD(again_consecutive,tc)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 }
ATF_TC_BODY(again_consecutive,tc)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);
ATF_TC_HEAD(follow_symlink_to_file,tc)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 }
ATF_TC_BODY(follow_symlink_to_file,tc)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);
ATF_TC_HEAD(follow_symlink_to_dir,tc)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 }
ATF_TC_BODY(follow_symlink_to_dir,tc)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);
ATF_TC_HEAD(follow_dead_symlink,tc)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 }
ATF_TC_BODY(follow_dead_symlink,tc)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);
ATF_TC_HEAD(skip,tc)247 ATF_TC_HEAD(skip, tc)
248 {
249 	atf_tc_set_md_var(tc, "descr",
250 	    "FTS_SKIP prevents descent into a directory");
251 }
ATF_TC_BODY(skip,tc)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);
ATF_TC_HEAD(clientptr_roundtrip,tc)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 }
ATF_TC_BODY(clientptr_roundtrip,tc)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);
ATF_TC_HEAD(get_stream_backpointer,tc)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 }
ATF_TC_BODY(get_stream_backpointer,tc)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 
ATF_TP_ADD_TCS(tp)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