-1 was used everywhere, but -EBADF or -EBADFD started being used in various places. Let's make things consistent in the new style. Note that there are two candidates: EBADF 9 Bad file descriptor EBADFD 77 File descriptor in bad state Since we're initializating the fd, we're just assigning a value that means "no fd yet", so it's just a bad file descriptor, and the first errno fits better. If instead we had a valid file descriptor that became invalid because of some operation or state change, the other errno would fit better. In some places, initialization is dropped if unnecessary.
504 lines
22 KiB
C
504 lines
22 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include "alloc-util.h"
|
|
#include "dirent-util.h"
|
|
#include "fd-util.h"
|
|
#include "fileio.h"
|
|
#include "missing_syscall.h"
|
|
#include "mountpoint-util.h"
|
|
#include "recurse-dir.h"
|
|
#include "sort-util.h"
|
|
|
|
#define DEFAULT_RECURSION_MAX 100
|
|
|
|
static int sort_func(struct dirent * const *a, struct dirent * const *b) {
|
|
return strcmp((*a)->d_name, (*b)->d_name);
|
|
}
|
|
|
|
static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) {
|
|
assert(de);
|
|
|
|
/* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */
|
|
|
|
return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ?
|
|
de->d_name[0] == '.' :
|
|
dot_or_dot_dot(de->d_name);
|
|
}
|
|
|
|
int readdir_all(int dir_fd,
|
|
RecurseDirFlags flags,
|
|
DirectoryEntries **ret) {
|
|
|
|
_cleanup_free_ DirectoryEntries *de = NULL;
|
|
struct dirent *entry;
|
|
DirectoryEntries *nde;
|
|
size_t add, sz, j;
|
|
int r;
|
|
|
|
assert(dir_fd >= 0);
|
|
|
|
/* Returns an array with pointers to "struct dirent" directory entries, optionally sorted. Free the
|
|
* array with readdir_all_freep().
|
|
*
|
|
* Start with space for up to 8 directory entries. We expect at least 2 ("." + ".."), hence hopefully
|
|
* 8 will cover most cases comprehensively. (Note that most likely a lot more entries will actually
|
|
* fit in the buffer, given we calculate maximum file name length here.) */
|
|
de = malloc(offsetof(DirectoryEntries, buffer) + DIRENT_SIZE_MAX * 8);
|
|
if (!de)
|
|
return -ENOMEM;
|
|
|
|
de->buffer_size = 0;
|
|
for (;;) {
|
|
size_t bs;
|
|
ssize_t n;
|
|
|
|
bs = MIN(MALLOC_SIZEOF_SAFE(de) - offsetof(DirectoryEntries, buffer), (size_t) SSIZE_MAX);
|
|
assert(bs > de->buffer_size);
|
|
|
|
n = getdents64(dir_fd, (uint8_t*) de->buffer + de->buffer_size, bs - de->buffer_size);
|
|
if (n < 0)
|
|
return -errno;
|
|
if (n == 0)
|
|
break;
|
|
|
|
msan_unpoison((uint8_t*) de->buffer + de->buffer_size, n);
|
|
|
|
de->buffer_size += n;
|
|
|
|
if (de->buffer_size < bs - DIRENT_SIZE_MAX) /* Still room for one more entry, then try to
|
|
* fill it up without growing the structure. */
|
|
continue;
|
|
|
|
if (bs >= SSIZE_MAX - offsetof(DirectoryEntries, buffer))
|
|
return -EFBIG;
|
|
bs = bs >= (SSIZE_MAX - offsetof(DirectoryEntries, buffer))/2 ? SSIZE_MAX - offsetof(DirectoryEntries, buffer) : bs * 2;
|
|
|
|
nde = realloc(de, bs);
|
|
if (!nde)
|
|
return -ENOMEM;
|
|
|
|
de = nde;
|
|
}
|
|
|
|
de->n_entries = 0;
|
|
FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
|
|
if (ignore_dirent(entry, flags))
|
|
continue;
|
|
|
|
if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) {
|
|
r = dirent_ensure_type(dir_fd, entry);
|
|
if (r == -ENOENT)
|
|
/* dentry gone by now? no problem, let's just suppress it */
|
|
continue;
|
|
if (r < 0)
|
|
return r;
|
|
}
|
|
|
|
de->n_entries++;
|
|
}
|
|
|
|
sz = ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size);
|
|
add = sizeof(struct dirent*) * de->n_entries;
|
|
if (add > SIZE_MAX - add)
|
|
return -ENOMEM;
|
|
|
|
nde = realloc(de, sz + add);
|
|
if (!nde)
|
|
return -ENOMEM;
|
|
|
|
de = nde;
|
|
de->entries = (struct dirent**) ((uint8_t*) de + ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size));
|
|
|
|
j = 0;
|
|
FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
|
|
if (ignore_dirent(entry, flags))
|
|
continue;
|
|
|
|
/* If d_type == DT_UNKNOWN that means we failed to ensure the type in the earlier loop and
|
|
* didn't include the dentry in de->n_entries and as such should skip it here as well. */
|
|
if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE) && entry->d_type == DT_UNKNOWN)
|
|
continue;
|
|
|
|
de->entries[j++] = entry;
|
|
}
|
|
assert(j == de->n_entries);
|
|
|
|
if (FLAGS_SET(flags, RECURSE_DIR_SORT))
|
|
typesafe_qsort(de->entries, de->n_entries, sort_func);
|
|
|
|
if (ret)
|
|
*ret = TAKE_PTR(de);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int recurse_dir(
|
|
int dir_fd,
|
|
const char *path,
|
|
unsigned statx_mask,
|
|
unsigned n_depth_max,
|
|
RecurseDirFlags flags,
|
|
recurse_dir_func_t func,
|
|
void *userdata) {
|
|
|
|
_cleanup_free_ DirectoryEntries *de = NULL;
|
|
STRUCT_STATX_DEFINE(root_sx);
|
|
int r;
|
|
|
|
assert(dir_fd >= 0);
|
|
assert(func);
|
|
|
|
/* This is a lot like ftw()/nftw(), but a lot more modern, i.e. built around openat()/statx()/O_PATH,
|
|
* and under the assumption that fds are not as 'expensive' as they used to be. */
|
|
|
|
if (n_depth_max == 0)
|
|
return -EOVERFLOW;
|
|
if (n_depth_max == UINT_MAX) /* special marker for "default" */
|
|
n_depth_max = DEFAULT_RECURSION_MAX;
|
|
|
|
if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
|
|
if (statx_mask != 0) {
|
|
r = statx_fallback(dir_fd, "", AT_EMPTY_PATH, statx_mask, &root_sx);
|
|
if (r < 0)
|
|
return r;
|
|
}
|
|
|
|
r = func(RECURSE_DIR_ENTER,
|
|
path,
|
|
-1, /* we have no parent fd */
|
|
dir_fd,
|
|
NULL, /* we have no dirent */
|
|
statx_mask != 0 ? &root_sx : NULL,
|
|
userdata);
|
|
if (IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY))
|
|
return 0;
|
|
if (r != RECURSE_DIR_CONTINUE)
|
|
return r;
|
|
}
|
|
|
|
/* Mask out RECURSE_DIR_ENSURE_TYPE so we can do it ourselves and avoid an extra statx() call. */
|
|
r = readdir_all(dir_fd, flags & ~RECURSE_DIR_ENSURE_TYPE, &de);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
for (size_t i = 0; i < de->n_entries; i++) {
|
|
_cleanup_close_ int inode_fd = -EBADF, subdir_fd = -EBADF;
|
|
_cleanup_free_ char *joined = NULL;
|
|
STRUCT_STATX_DEFINE(sx);
|
|
bool sx_valid = false;
|
|
const char *p;
|
|
|
|
/* For each directory entry we'll do one of the following:
|
|
*
|
|
* 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)
|
|
* 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)
|
|
* 3) Otherwise, we'll statx() the directory entry via the directory fd we are currently looking at (if requested)
|
|
*/
|
|
|
|
if (path) {
|
|
joined = path_join(path, de->entries[i]->d_name);
|
|
if (!joined)
|
|
return -ENOMEM;
|
|
|
|
p = joined;
|
|
} else
|
|
p = de->entries[i]->d_name;
|
|
|
|
if (IN_SET(de->entries[i]->d_type, DT_UNKNOWN, DT_DIR)) {
|
|
subdir_fd = openat(dir_fd, de->entries[i]->d_name, O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
|
|
if (subdir_fd < 0) {
|
|
if (errno == ENOENT) /* Vanished by now, go for next file immediately */
|
|
continue;
|
|
|
|
/* If it is a subdir but we failed to open it, then fail */
|
|
if (!IN_SET(errno, ENOTDIR, ELOOP)) {
|
|
log_debug_errno(errno, "Failed to open directory '%s': %m", p);
|
|
|
|
assert(errno <= RECURSE_DIR_SKIP_OPEN_DIR_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE);
|
|
|
|
r = func(RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE + errno,
|
|
p,
|
|
dir_fd,
|
|
-1,
|
|
de->entries[i],
|
|
NULL,
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
|
|
return r;
|
|
|
|
continue;
|
|
}
|
|
|
|
/* If it's not a subdir, then let's handle it like a regular inode below */
|
|
|
|
} else {
|
|
/* If we managed to get a DIR* off the inode, it's definitely a directory. */
|
|
de->entries[i]->d_type = DT_DIR;
|
|
|
|
if (statx_mask != 0 || (flags & RECURSE_DIR_SAME_MOUNT)) {
|
|
r = statx_fallback(subdir_fd, "", AT_EMPTY_PATH, statx_mask, &sx);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
sx_valid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (subdir_fd < 0) {
|
|
/* It's not a subdirectory. */
|
|
|
|
if (flags & RECURSE_DIR_INODE_FD) {
|
|
|
|
inode_fd = openat(dir_fd, de->entries[i]->d_name, O_PATH|O_NOFOLLOW|O_CLOEXEC);
|
|
if (inode_fd < 0) {
|
|
if (errno == ENOENT) /* Vanished by now, go for next file immediately */
|
|
continue;
|
|
|
|
log_debug_errno(errno, "Failed to open directory entry '%s': %m", p);
|
|
|
|
assert(errno <= RECURSE_DIR_SKIP_OPEN_INODE_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE);
|
|
|
|
r = func(RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE + errno,
|
|
p,
|
|
dir_fd,
|
|
-1,
|
|
de->entries[i],
|
|
NULL,
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
|
|
return r;
|
|
|
|
continue;
|
|
}
|
|
|
|
/* If we open the inode, then verify it's actually a non-directory, like we
|
|
* assume. Let's guarantee that we never pass statx data of a directory where
|
|
* caller expects a non-directory */
|
|
|
|
r = statx_fallback(inode_fd, "", AT_EMPTY_PATH, statx_mask | STATX_TYPE, &sx);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
assert(sx.stx_mask & STATX_TYPE);
|
|
sx_valid = true;
|
|
|
|
if (S_ISDIR(sx.stx_mode)) {
|
|
/* What? It's a directory now? Then someone must have quickly
|
|
* replaced it. Let's handle that gracefully: convert it to a
|
|
* directory fd — which should be riskless now that we pinned the
|
|
* inode. */
|
|
|
|
subdir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_CLOEXEC);
|
|
if (subdir_fd < 0)
|
|
return subdir_fd;
|
|
|
|
inode_fd = safe_close(inode_fd);
|
|
}
|
|
|
|
} else if (statx_mask != 0 || (de->entries[i]->d_type == DT_UNKNOWN && (flags & RECURSE_DIR_ENSURE_TYPE))) {
|
|
|
|
r = statx_fallback(dir_fd, de->entries[i]->d_name, AT_SYMLINK_NOFOLLOW, statx_mask | STATX_TYPE, &sx);
|
|
if (r == -ENOENT) /* Vanished by now? Go for next file immediately */
|
|
continue;
|
|
if (r < 0) {
|
|
log_debug_errno(r, "Failed to stat directory entry '%s': %m", p);
|
|
|
|
assert(errno <= RECURSE_DIR_SKIP_STAT_INODE_ERROR_MAX - RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE);
|
|
|
|
r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + -r,
|
|
p,
|
|
dir_fd,
|
|
-1,
|
|
de->entries[i],
|
|
NULL,
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
|
|
return r;
|
|
|
|
continue;
|
|
}
|
|
|
|
assert(sx.stx_mask & STATX_TYPE);
|
|
sx_valid = true;
|
|
|
|
if (S_ISDIR(sx.stx_mode)) {
|
|
/* So it suddenly is a directory, but we couldn't open it as such
|
|
* earlier? That is weird, and probably means somebody is racing
|
|
* against us. We could of course retry and open it as a directory
|
|
* again, but the chance to win here is limited. Hence, let's
|
|
* propagate this as EISDIR error instead. That way we make this
|
|
* something that can be reasonably handled, even though we give the
|
|
* guarantee that RECURSE_DIR_ENTRY is strictly issued for
|
|
* non-directory dirents. */
|
|
|
|
log_debug_errno(r, "Non-directory entry '%s' suddenly became a directory: %m", p);
|
|
|
|
r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + EISDIR,
|
|
p,
|
|
dir_fd,
|
|
-1,
|
|
de->entries[i],
|
|
NULL,
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
|
|
return r;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sx_valid) {
|
|
/* Copy over the data we acquired through statx() if we acquired any */
|
|
if (sx.stx_mask & STATX_TYPE) {
|
|
assert((subdir_fd < 0) == !S_ISDIR(sx.stx_mode));
|
|
de->entries[i]->d_type = IFTODT(sx.stx_mode);
|
|
}
|
|
|
|
if (sx.stx_mask & STATX_INO)
|
|
de->entries[i]->d_ino = sx.stx_ino;
|
|
}
|
|
|
|
if (subdir_fd >= 0) {
|
|
if (FLAGS_SET(flags, RECURSE_DIR_SAME_MOUNT)) {
|
|
bool is_mount;
|
|
|
|
if (sx_valid && FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT))
|
|
is_mount = FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT);
|
|
else {
|
|
r = fd_is_mount_point(dir_fd, de->entries[i]->d_name, 0);
|
|
if (r < 0)
|
|
log_debug_errno(r, "Failed to determine whether %s is a submount, assuming not: %m", p);
|
|
|
|
is_mount = r > 0;
|
|
}
|
|
|
|
if (is_mount) {
|
|
r = func(RECURSE_DIR_SKIP_MOUNT,
|
|
p,
|
|
dir_fd,
|
|
subdir_fd,
|
|
de->entries[i],
|
|
statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
|
|
return r;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (n_depth_max <= 1) {
|
|
/* When we reached max depth, generate a special event */
|
|
|
|
r = func(RECURSE_DIR_SKIP_DEPTH,
|
|
p,
|
|
dir_fd,
|
|
subdir_fd,
|
|
de->entries[i],
|
|
statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
|
|
return r;
|
|
|
|
continue;
|
|
}
|
|
|
|
r = func(RECURSE_DIR_ENTER,
|
|
p,
|
|
dir_fd,
|
|
subdir_fd,
|
|
de->entries[i],
|
|
statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
|
|
userdata);
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (r == RECURSE_DIR_SKIP_ENTRY)
|
|
continue;
|
|
if (r != RECURSE_DIR_CONTINUE)
|
|
return r;
|
|
|
|
r = recurse_dir(subdir_fd,
|
|
p,
|
|
statx_mask,
|
|
n_depth_max - 1,
|
|
flags &~ RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */
|
|
func,
|
|
userdata);
|
|
if (r != 0)
|
|
return r;
|
|
|
|
r = func(RECURSE_DIR_LEAVE,
|
|
p,
|
|
dir_fd,
|
|
subdir_fd,
|
|
de->entries[i],
|
|
statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
|
|
userdata);
|
|
} else
|
|
/* Non-directory inode */
|
|
r = func(RECURSE_DIR_ENTRY,
|
|
p,
|
|
dir_fd,
|
|
inode_fd,
|
|
de->entries[i],
|
|
statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
|
|
userdata);
|
|
|
|
|
|
if (r == RECURSE_DIR_LEAVE_DIRECTORY)
|
|
break;
|
|
if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
|
|
return r;
|
|
}
|
|
|
|
if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
|
|
|
|
r = func(RECURSE_DIR_LEAVE,
|
|
path,
|
|
-1,
|
|
dir_fd,
|
|
NULL,
|
|
statx_mask != 0 ? &root_sx : NULL,
|
|
userdata);
|
|
if (!IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
|
|
return r;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int recurse_dir_at(
|
|
int atfd,
|
|
const char *path,
|
|
unsigned statx_mask,
|
|
unsigned n_depth_max,
|
|
RecurseDirFlags flags,
|
|
recurse_dir_func_t func,
|
|
void *userdata) {
|
|
|
|
_cleanup_close_ int fd = -EBADF;
|
|
|
|
assert(atfd >= 0 || atfd == AT_FDCWD);
|
|
assert(func);
|
|
|
|
fd = openat(atfd, path ?: ".", O_DIRECTORY|O_CLOEXEC);
|
|
if (fd < 0)
|
|
return -errno;
|
|
|
|
return recurse_dir(fd, path, statx_mask, n_depth_max, flags, func, userdata);
|
|
}
|