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