]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/basic/recurse-dir.c
iovec-util: add exported constant empty but valid (i.e. non-NULL) iovec
[thirdparty/systemd.git] / src / basic / recurse-dir.c
CommitLineData
b5a07e52
LP
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3#include "alloc-util.h"
4#include "dirent-util.h"
5#include "fd-util.h"
6#include "fileio.h"
4d564427 7#include "fs-util.h"
6393b847 8#include "missing_syscall.h"
b5a07e52
LP
9#include "mountpoint-util.h"
10#include "recurse-dir.h"
11#include "sort-util.h"
12
13#define DEFAULT_RECURSION_MAX 100
14
15static int sort_func(struct dirent * const *a, struct dirent * const *b) {
16 return strcmp((*a)->d_name, (*b)->d_name);
17}
18
6393b847
LP
19static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) {
20 assert(de);
b5a07e52 21
6393b847 22 /* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */
b5a07e52 23
6393b847
LP
24 return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ?
25 de->d_name[0] == '.' :
26 dot_or_dot_dot(de->d_name);
b5a07e52
LP
27}
28
6393b847 29int readdir_all(int dir_fd,
b5a07e52 30 RecurseDirFlags flags,
6393b847 31 DirectoryEntries **ret) {
b5a07e52 32
6393b847 33 _cleanup_free_ DirectoryEntries *de = NULL;
a4e70ef7 34 struct dirent *entry;
6393b847
LP
35 DirectoryEntries *nde;
36 size_t add, sz, j;
a0a4c578 37 int r;
b5a07e52 38
6393b847 39 assert(dir_fd >= 0);
b5a07e52
LP
40
41 /* Returns an array with pointers to "struct dirent" directory entries, optionally sorted. Free the
ca664db2
LP
42 * array with readdir_all_freep().
43 *
44 * Start with space for up to 8 directory entries. We expect at least 2 ("." + ".."), hence hopefully
6393b847
LP
45 * 8 will cover most cases comprehensively. (Note that most likely a lot more entries will actually
46 * fit in the buffer, given we calculate maximum file name length here.) */
47 de = malloc(offsetof(DirectoryEntries, buffer) + DIRENT_SIZE_MAX * 8);
48 if (!de)
49 return -ENOMEM;
50
51 de->buffer_size = 0;
b5a07e52 52 for (;;) {
6393b847
LP
53 size_t bs;
54 ssize_t n;
b5a07e52 55
6393b847
LP
56 bs = MIN(MALLOC_SIZEOF_SAFE(de) - offsetof(DirectoryEntries, buffer), (size_t) SSIZE_MAX);
57 assert(bs > de->buffer_size);
b5a07e52 58
a4e70ef7 59 n = getdents64(dir_fd, (uint8_t*) de->buffer + de->buffer_size, bs - de->buffer_size);
6393b847 60 if (n < 0)
b5a07e52 61 return -errno;
6393b847
LP
62 if (n == 0)
63 break;
b5a07e52 64
0dbce03c
LP
65 msan_unpoison((uint8_t*) de->buffer + de->buffer_size, n);
66
6393b847
LP
67 de->buffer_size += n;
68
69 if (de->buffer_size < bs - DIRENT_SIZE_MAX) /* Still room for one more entry, then try to
70 * fill it up without growing the structure. */
b5a07e52
LP
71 continue;
72
6393b847
LP
73 if (bs >= SSIZE_MAX - offsetof(DirectoryEntries, buffer))
74 return -EFBIG;
75 bs = bs >= (SSIZE_MAX - offsetof(DirectoryEntries, buffer))/2 ? SSIZE_MAX - offsetof(DirectoryEntries, buffer) : bs * 2;
b5a07e52 76
6393b847
LP
77 nde = realloc(de, bs);
78 if (!nde)
b5a07e52
LP
79 return -ENOMEM;
80
6393b847
LP
81 de = nde;
82 }
83
84 de->n_entries = 0;
a4e70ef7 85 FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
6393b847
LP
86 if (ignore_dirent(entry, flags))
87 continue;
b5a07e52 88
a0a4c578
DDM
89 if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) {
90 r = dirent_ensure_type(dir_fd, entry);
91 if (r == -ENOENT)
92 /* dentry gone by now? no problem, let's just suppress it */
93 continue;
94 if (r < 0)
95 return r;
96 }
97
6393b847
LP
98 de->n_entries++;
99 }
100
101 sz = ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size);
102 add = sizeof(struct dirent*) * de->n_entries;
103 if (add > SIZE_MAX - add)
104 return -ENOMEM;
105
106 nde = realloc(de, sz + add);
107 if (!nde)
108 return -ENOMEM;
109
110 de = nde;
111 de->entries = (struct dirent**) ((uint8_t*) de + ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size));
112
113 j = 0;
a4e70ef7 114 FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
6393b847
LP
115 if (ignore_dirent(entry, flags))
116 continue;
117
a0a4c578
DDM
118 /* If d_type == DT_UNKNOWN that means we failed to ensure the type in the earlier loop and
119 * didn't include the dentry in de->n_entries and as such should skip it here as well. */
120 if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE) && entry->d_type == DT_UNKNOWN)
121 continue;
122
6393b847 123 de->entries[j++] = entry;
b5a07e52 124 }
a0a4c578 125 assert(j == de->n_entries);
b5a07e52
LP
126
127 if (FLAGS_SET(flags, RECURSE_DIR_SORT))
6393b847 128 typesafe_qsort(de->entries, de->n_entries, sort_func);
b5a07e52
LP
129
130 if (ret)
6393b847 131 *ret = TAKE_PTR(de);
b5a07e52 132
6393b847 133 return 0;
b5a07e52
LP
134}
135
4d564427
LP
136int readdir_all_at(int fd, const char *path, RecurseDirFlags flags, DirectoryEntries **ret) {
137 _cleanup_close_ int dir_fd = -EBADF;
138
139 assert(fd >= 0 || fd == AT_FDCWD);
140
e40b11be 141 dir_fd = xopenat(fd, path, O_DIRECTORY|O_CLOEXEC);
4d564427
LP
142 if (dir_fd < 0)
143 return dir_fd;
144
145 return readdir_all(dir_fd, flags, ret);
146}
147
b5a07e52 148int recurse_dir(
6393b847 149 int dir_fd,
b5a07e52
LP
150 const char *path,
151 unsigned statx_mask,
152 unsigned n_depth_max,
153 RecurseDirFlags flags,
154 recurse_dir_func_t func,
155 void *userdata) {
156
6393b847 157 _cleanup_free_ DirectoryEntries *de = NULL;
b21ec07b 158 STRUCT_STATX_DEFINE(root_sx);
6393b847 159 int r;
b5a07e52 160
6393b847 161 assert(dir_fd >= 0);
b5a07e52
LP
162 assert(func);
163
6393b847
LP
164 /* This is a lot like ftw()/nftw(), but a lot more modern, i.e. built around openat()/statx()/O_PATH,
165 * and under the assumption that fds are not as 'expensive' as they used to be. */
b5a07e52
LP
166
167 if (n_depth_max == 0)
168 return -EOVERFLOW;
169 if (n_depth_max == UINT_MAX) /* special marker for "default" */
170 n_depth_max = DEFAULT_RECURSION_MAX;
171
b21ec07b
LP
172 if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
173 if (statx_mask != 0) {
174 r = statx_fallback(dir_fd, "", AT_EMPTY_PATH, statx_mask, &root_sx);
175 if (r < 0)
176 return r;
177 }
178
179 r = func(RECURSE_DIR_ENTER,
180 path,
181 -1, /* we have no parent fd */
182 dir_fd,
183 NULL, /* we have no dirent */
184 statx_mask != 0 ? &root_sx : NULL,
185 userdata);
186 if (IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY))
187 return 0;
188 if (r != RECURSE_DIR_CONTINUE)
189 return r;
190 }
191
a0a4c578
DDM
192 /* Mask out RECURSE_DIR_ENSURE_TYPE so we can do it ourselves and avoid an extra statx() call. */
193 r = readdir_all(dir_fd, flags & ~RECURSE_DIR_ENSURE_TYPE, &de);
6393b847
LP
194 if (r < 0)
195 return r;
b5a07e52 196
6393b847 197 for (size_t i = 0; i < de->n_entries; i++) {
254d1313 198 _cleanup_close_ int inode_fd = -EBADF, subdir_fd = -EBADF;
b5a07e52 199 _cleanup_free_ char *joined = NULL;
b5a07e52
LP
200 STRUCT_STATX_DEFINE(sx);
201 bool sx_valid = false;
202 const char *p;
203
204 /* For each directory entry we'll do one of the following:
205 *
6393b847
LP
206 * 1) If the entry refers to a directory, we'll open it as O_DIRECTORY 'subdir_fd' and then statx() the opened directory via that new fd (if requested)
207 * 2) Otherwise, if RECURSE_DIR_INODE_FD is set we'll open it as O_PATH 'inode_fd' and then statx() the opened inode via that new fd (if requested)
208 * 3) Otherwise, we'll statx() the directory entry via the directory fd we are currently looking at (if requested)
b5a07e52
LP
209 */
210
211 if (path) {
6393b847 212 joined = path_join(path, de->entries[i]->d_name);
b5a07e52
LP
213 if (!joined)
214 return -ENOMEM;
215
216 p = joined;
217 } else
6393b847 218 p = de->entries[i]->d_name;
b5a07e52 219
6393b847
LP
220 if (IN_SET(de->entries[i]->d_type, DT_UNKNOWN, DT_DIR)) {
221 subdir_fd = openat(dir_fd, de->entries[i]->d_name, O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
222 if (subdir_fd < 0) {
b5a07e52
LP
223 if (errno == ENOENT) /* Vanished by now, go for next file immediately */
224 continue;
225
226 /* If it is a subdir but we failed to open it, then fail */
227 if (!IN_SET(errno, ENOTDIR, ELOOP)) {
228 log_debug_errno(errno, "Failed to open directory '%s': %m", p);
229
230 assert(errno <= RECURSE_DIR_SKIP_OPEN_DIR_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE);
231
232 r = func(RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE + errno,
233 p,
6393b847 234 dir_fd,
b5a07e52 235 -1,
6393b847 236 de->entries[i],
b5a07e52
LP
237 NULL,
238 userdata);
239 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
240 break;
241 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
242 return r;
243
244 continue;
245 }
246
247 /* If it's not a subdir, then let's handle it like a regular inode below */
248
249 } else {
250 /* If we managed to get a DIR* off the inode, it's definitely a directory. */
6393b847 251 de->entries[i]->d_type = DT_DIR;
b5a07e52
LP
252
253 if (statx_mask != 0 || (flags & RECURSE_DIR_SAME_MOUNT)) {
6393b847 254 r = statx_fallback(subdir_fd, "", AT_EMPTY_PATH, statx_mask, &sx);
b5a07e52
LP
255 if (r < 0)
256 return r;
257
258 sx_valid = true;
259 }
260 }
261 }
262
6393b847 263 if (subdir_fd < 0) {
b5a07e52
LP
264 /* It's not a subdirectory. */
265
266 if (flags & RECURSE_DIR_INODE_FD) {
267
6393b847 268 inode_fd = openat(dir_fd, de->entries[i]->d_name, O_PATH|O_NOFOLLOW|O_CLOEXEC);
b5a07e52
LP
269 if (inode_fd < 0) {
270 if (errno == ENOENT) /* Vanished by now, go for next file immediately */
271 continue;
272
273 log_debug_errno(errno, "Failed to open directory entry '%s': %m", p);
274
275 assert(errno <= RECURSE_DIR_SKIP_OPEN_INODE_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE);
276
277 r = func(RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE + errno,
278 p,
6393b847 279 dir_fd,
b5a07e52 280 -1,
6393b847 281 de->entries[i],
b5a07e52
LP
282 NULL,
283 userdata);
284 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
285 break;
286 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
287 return r;
288
289 continue;
290 }
291
292 /* If we open the inode, then verify it's actually a non-directory, like we
293 * assume. Let's guarantee that we never pass statx data of a directory where
294 * caller expects a non-directory */
295
296 r = statx_fallback(inode_fd, "", AT_EMPTY_PATH, statx_mask | STATX_TYPE, &sx);
297 if (r < 0)
298 return r;
299
300 assert(sx.stx_mask & STATX_TYPE);
301 sx_valid = true;
302
303 if (S_ISDIR(sx.stx_mode)) {
304 /* What? It's a directory now? Then someone must have quickly
305 * replaced it. Let's handle that gracefully: convert it to a
ba669952 306 * directory fd — which should be riskless now that we pinned the
b5a07e52
LP
307 * inode. */
308
739d9cae 309 subdir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_CLOEXEC);
6393b847 310 if (subdir_fd < 0)
739d9cae 311 return subdir_fd;
b5a07e52
LP
312
313 inode_fd = safe_close(inode_fd);
314 }
315
6393b847 316 } else if (statx_mask != 0 || (de->entries[i]->d_type == DT_UNKNOWN && (flags & RECURSE_DIR_ENSURE_TYPE))) {
b5a07e52 317
6393b847 318 r = statx_fallback(dir_fd, de->entries[i]->d_name, AT_SYMLINK_NOFOLLOW, statx_mask | STATX_TYPE, &sx);
b5a07e52
LP
319 if (r == -ENOENT) /* Vanished by now? Go for next file immediately */
320 continue;
321 if (r < 0) {
322 log_debug_errno(r, "Failed to stat directory entry '%s': %m", p);
323
324 assert(errno <= RECURSE_DIR_SKIP_STAT_INODE_ERROR_MAX - RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE);
325
326 r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + -r,
327 p,
6393b847 328 dir_fd,
b5a07e52 329 -1,
6393b847 330 de->entries[i],
b5a07e52
LP
331 NULL,
332 userdata);
333 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
334 break;
335 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
336 return r;
337
338 continue;
339 }
340
341 assert(sx.stx_mask & STATX_TYPE);
342 sx_valid = true;
343
344 if (S_ISDIR(sx.stx_mode)) {
345 /* So it suddenly is a directory, but we couldn't open it as such
346 * earlier? That is weird, and probably means somebody is racing
347 * against us. We could of course retry and open it as a directory
348 * again, but the chance to win here is limited. Hence, let's
349 * propagate this as EISDIR error instead. That way we make this
350 * something that can be reasonably handled, even though we give the
351 * guarantee that RECURSE_DIR_ENTRY is strictly issued for
352 * non-directory dirents. */
353
354 log_debug_errno(r, "Non-directory entry '%s' suddenly became a directory: %m", p);
355
356 r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + EISDIR,
357 p,
6393b847 358 dir_fd,
b5a07e52 359 -1,
6393b847 360 de->entries[i],
b5a07e52
LP
361 NULL,
362 userdata);
363 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
364 break;
365 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
366 return r;
367
368 continue;
369 }
370 }
371 }
372
373 if (sx_valid) {
374 /* Copy over the data we acquired through statx() if we acquired any */
375 if (sx.stx_mask & STATX_TYPE) {
6393b847
LP
376 assert((subdir_fd < 0) == !S_ISDIR(sx.stx_mode));
377 de->entries[i]->d_type = IFTODT(sx.stx_mode);
b5a07e52
LP
378 }
379
380 if (sx.stx_mask & STATX_INO)
6393b847 381 de->entries[i]->d_ino = sx.stx_ino;
b5a07e52
LP
382 }
383
6393b847 384 if (subdir_fd >= 0) {
b5a07e52
LP
385 if (FLAGS_SET(flags, RECURSE_DIR_SAME_MOUNT)) {
386 bool is_mount;
387
388 if (sx_valid && FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT))
389 is_mount = FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT);
390 else {
6393b847 391 r = fd_is_mount_point(dir_fd, de->entries[i]->d_name, 0);
b5a07e52
LP
392 if (r < 0)
393 log_debug_errno(r, "Failed to determine whether %s is a submount, assuming not: %m", p);
394
395 is_mount = r > 0;
396 }
397
398 if (is_mount) {
399 r = func(RECURSE_DIR_SKIP_MOUNT,
400 p,
6393b847
LP
401 dir_fd,
402 subdir_fd,
403 de->entries[i],
b5a07e52
LP
404 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
405 userdata);
406 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
407 break;
408 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
409 return r;
410
411 continue;
412 }
413 }
414
415 if (n_depth_max <= 1) {
416 /* When we reached max depth, generate a special event */
417
418 r = func(RECURSE_DIR_SKIP_DEPTH,
419 p,
6393b847
LP
420 dir_fd,
421 subdir_fd,
422 de->entries[i],
b5a07e52
LP
423 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
424 userdata);
425 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
426 break;
427 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
428 return r;
429
430 continue;
431 }
432
433 r = func(RECURSE_DIR_ENTER,
434 p,
6393b847
LP
435 dir_fd,
436 subdir_fd,
437 de->entries[i],
b5a07e52
LP
438 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
439 userdata);
440 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
441 break;
442 if (r == RECURSE_DIR_SKIP_ENTRY)
443 continue;
444 if (r != RECURSE_DIR_CONTINUE)
445 return r;
446
6393b847 447 r = recurse_dir(subdir_fd,
b5a07e52
LP
448 p,
449 statx_mask,
450 n_depth_max - 1,
b21ec07b 451 flags &~ RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */
b5a07e52
LP
452 func,
453 userdata);
454 if (r != 0)
455 return r;
456
457 r = func(RECURSE_DIR_LEAVE,
458 p,
6393b847
LP
459 dir_fd,
460 subdir_fd,
461 de->entries[i],
b5a07e52
LP
462 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
463 userdata);
464 } else
465 /* Non-directory inode */
466 r = func(RECURSE_DIR_ENTRY,
467 p,
6393b847 468 dir_fd,
b5a07e52 469 inode_fd,
6393b847 470 de->entries[i],
b5a07e52
LP
471 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
472 userdata);
473
474
475 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
476 break;
477 if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
478 return r;
479 }
480
b21ec07b
LP
481 if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
482
483 r = func(RECURSE_DIR_LEAVE,
484 path,
485 -1,
486 dir_fd,
487 NULL,
488 statx_mask != 0 ? &root_sx : NULL,
489 userdata);
490 if (!IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
491 return r;
492 }
493
b5a07e52
LP
494 return 0;
495}
496
497int recurse_dir_at(
498 int atfd,
499 const char *path,
500 unsigned statx_mask,
501 unsigned n_depth_max,
502 RecurseDirFlags flags,
503 recurse_dir_func_t func,
504 void *userdata) {
505
254d1313 506 _cleanup_close_ int fd = -EBADF;
6393b847
LP
507
508 assert(atfd >= 0 || atfd == AT_FDCWD);
509 assert(func);
510
aa3cc58a 511 fd = openat(atfd, path ?: ".", O_DIRECTORY|O_CLOEXEC);
6393b847 512 if (fd < 0)
b5a07e52
LP
513 return -errno;
514
6393b847 515 return recurse_dir(fd, path, statx_mask, n_depth_max, flags, func, userdata);
b5a07e52 516}