diff options
author | トトも <85485984+ElectronicsArchiver@users.noreply.github.com> | 2022-04-16 20:18:56 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-16 14:18:56 -0400 |
commit | 33cc3b9dd7bf3dae1c6cf86e46bb4923f96e7fff (patch) | |
tree | 02fb808d19aee560ac9d381ca5a52509881cdd44 /src/eval.c | |
parent | 8f5a73a6585bd425807430fd80ce1e3a737f4c5f (diff) | |
download | bfs-33cc3b9dd7bf3dae1c6cf86e46bb4923f96e7fff.tar.xz |
Source / Include Folder (#88)
Moved Source Files Into `src` Folder
Diffstat (limited to 'src/eval.c')
-rw-r--r-- | src/eval.c | 1644 |
1 files changed, 1644 insertions, 0 deletions
diff --git a/src/eval.c b/src/eval.c new file mode 100644 index 0000000..1d0a6f2 --- /dev/null +++ b/src/eval.c @@ -0,0 +1,1644 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted. * + * * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * + ****************************************************************************/ + +/** + * Implementation of all the literal expressions. + */ + +#include "eval.h" +#include "bar.h" +#include "bftw.h" +#include "color.h" +#include "ctx.h" +#include "darray.h" +#include "diag.h" +#include "dir.h" +#include "dstring.h" +#include "exec.h" +#include "expr.h" +#include "fsade.h" +#include "mtab.h" +#include "printf.h" +#include "pwcache.h" +#include "stat.h" +#include "trie.h" +#include "util.h" +#include "xregex.h" +#include "xtime.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <fnmatch.h> +#include <grp.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> +#include <wchar.h> + +struct bfs_eval { + /** Data about the current file. */ + const struct BFTW *ftwbuf; + /** The bfs context. */ + const struct bfs_ctx *ctx; + /** The bftw() callback return value. */ + enum bftw_action action; + /** The bfs_eval() return value. */ + int *ret; + /** Whether to quit immediately. */ + bool quit; +}; + +/** + * Print an error message. + */ +BFS_FORMATTER(2, 3) +static void eval_error(struct bfs_eval *state, const char *format, ...) { + // By POSIX, any errors should be accompanied by a non-zero exit status + *state->ret = EXIT_FAILURE; + + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + CFILE *cerr = ctx->cerr; + + bfs_error(ctx, "%pP: ", state->ftwbuf); + + va_list args; + va_start(args, format); + errno = error; + cvfprintf(cerr, format, args); + va_end(args); +} + +/** + * Check if an error should be ignored. + */ +static bool eval_should_ignore(const struct bfs_eval *state, int error) { + return state->ctx->ignore_races + && is_nonexistence_error(error) + && state->ftwbuf->depth > 0; +} + +/** + * Report an error that occurs during evaluation. + */ +static void eval_report_error(struct bfs_eval *state) { + if (!eval_should_ignore(state, errno)) { + eval_error(state, "%m.\n"); + } +} + +/** + * Perform a bfs_stat() call if necessary. + */ +static const struct bfs_stat *eval_stat(struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + const struct bfs_stat *ret = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!ret) { + eval_report_error(state); + } + return ret; +} + +/** + * Get the difference (in seconds) between two struct timespecs. + */ +static time_t timespec_diff(const struct timespec *lhs, const struct timespec *rhs) { + time_t ret = lhs->tv_sec - rhs->tv_sec; + if (lhs->tv_nsec < rhs->tv_nsec) { + --ret; + } + return ret; +} + +bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) { + switch (expr->int_cmp) { + case BFS_INT_EQUAL: + return n == expr->num; + case BFS_INT_LESS: + return n < expr->num; + case BFS_INT_GREATER: + return n > expr->num; + } + + assert(!"Invalid comparison mode"); + return false; +} + +/** + * -true test. + */ +bool eval_true(const struct bfs_expr *expr, struct bfs_eval *state) { + return true; +} + +/** + * -false test. + */ +bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state) { + return false; +} + +/** + * -executable, -readable, -writable tests. + */ +bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, expr->num) == 0; +} + +/** + * -acl test. + */ +bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state) { + int ret = bfs_check_acl(state->ftwbuf); + if (ret >= 0) { + return ret; + } else { + eval_report_error(state); + return false; + } +} + +/** + * -capable test. + */ +bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state) { + int ret = bfs_check_capabilities(state->ftwbuf); + if (ret >= 0) { + return ret; + } else { + eval_report_error(state); + return false; + } +} + +/** + * Get the given timespec field out of a stat buffer. + */ +static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct bfs_eval *state) { + const struct timespec *ret = bfs_stat_time(statbuf, field); + if (!ret) { + eval_error(state, "Couldn't get file %s: %m.\n", bfs_stat_field_name(field)); + } + return ret; +} + +/** + * -[aBcm]?newer tests. + */ +bool eval_newer(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + const struct timespec *time = eval_stat_time(statbuf, expr->stat_field, state); + if (!time) { + return false; + } + + return time->tv_sec > expr->reftime.tv_sec + || (time->tv_sec == expr->reftime.tv_sec && time->tv_nsec > expr->reftime.tv_nsec); +} + +/** + * -[aBcm]{min,time} tests. + */ +bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + const struct timespec *time = eval_stat_time(statbuf, expr->stat_field, state); + if (!time) { + return false; + } + + time_t diff = timespec_diff(&expr->reftime, time); + switch (expr->time_unit) { + case BFS_DAYS: + diff /= 60*24; + BFS_FALLTHROUGH; + case BFS_MINUTES: + diff /= 60; + BFS_FALLTHROUGH; + case BFS_SECONDS: + break; + } + + return bfs_expr_cmp(expr, diff); +} + +/** + * -used test. + */ +bool eval_used(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + const struct timespec *atime = eval_stat_time(statbuf, BFS_STAT_ATIME, state); + const struct timespec *ctime = eval_stat_time(statbuf, BFS_STAT_CTIME, state); + if (!atime || !ctime) { + return false; + } + + long long diff = timespec_diff(atime, ctime); + if (diff < 0) { + return false; + } + + long long day_seconds = 60*60*24; + diff = (diff + day_seconds - 1) / day_seconds; + return bfs_expr_cmp(expr, diff); +} + +/** + * -gid test. + */ +bool eval_gid(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + return bfs_expr_cmp(expr, statbuf->gid); +} + +/** + * -uid test. + */ +bool eval_uid(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + return bfs_expr_cmp(expr, statbuf->uid); +} + +/** + * -nogroup test. + */ +bool eval_nogroup(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); + if (!groups) { + eval_report_error(state); + return false; + } + + return bfs_getgrgid(groups, statbuf->gid) == NULL; +} + +/** + * -nouser test. + */ +bool eval_nouser(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + const struct bfs_users *users = bfs_ctx_users(state->ctx); + if (!users) { + eval_report_error(state); + return false; + } + + return bfs_getpwuid(users, statbuf->uid) == NULL; +} + +/** + * -delete action. + */ +bool eval_delete(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + + // Don't try to delete the current directory + if (strcmp(ftwbuf->path, ".") == 0) { + return true; + } + + int flag = 0; + + // We need to know the actual type of the path, not what it points to + enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_NOFOLLOW); + if (type == BFS_DIR) { + flag |= AT_REMOVEDIR; + } else if (type == BFS_ERROR) { + eval_report_error(state); + return false; + } + + if (unlinkat(ftwbuf->at_fd, ftwbuf->at_path, flag) != 0) { + eval_report_error(state); + return false; + } + + return true; +} + +/** Finish any pending -exec ... + operations. */ +static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *ctx) { + int ret = 0; + + if (expr->eval_fn == eval_exec) { + if (bfs_exec_finish(expr->exec) != 0) { + if (errno != 0) { + bfs_error(ctx, "%s %s: %m.\n", expr->argv[0], expr->argv[1]); + } + ret = -1; + } + } else if (bfs_expr_has_children(expr)) { + if (expr->lhs && eval_exec_finish(expr->lhs, ctx) != 0) { + ret = -1; + } + if (expr->rhs && eval_exec_finish(expr->rhs, ctx) != 0) { + ret = -1; + } + } + + return ret; +} + +/** + * -exec[dir]/-ok[dir] actions. + */ +bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) { + bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0; + if (errno != 0) { + eval_error(state, "%s %s: %m.\n", expr->argv[0], expr->argv[1]); + } + return ret; +} + +/** + * -exit action. + */ +bool eval_exit(const struct bfs_expr *expr, struct bfs_eval *state) { + state->action = BFTW_STOP; + *state->ret = expr->num; + state->quit = true; + return true; +} + +/** + * -depth N test. + */ +bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state) { + return bfs_expr_cmp(expr, state->ftwbuf->depth); +} + +/** + * -empty test. + */ +bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state) { + bool ret = false; + const struct BFTW *ftwbuf = state->ftwbuf; + + if (ftwbuf->type == BFS_DIR) { + struct bfs_dir *dir = bfs_opendir(ftwbuf->at_fd, ftwbuf->at_path); + if (!dir) { + eval_report_error(state); + goto done; + } + + int did_read = bfs_readdir(dir, NULL); + if (did_read < 0) { + eval_report_error(state); + } else { + ret = !did_read; + } + + bfs_closedir(dir); + } else if (ftwbuf->type == BFS_REG) { + const struct bfs_stat *statbuf = eval_stat(state); + if (statbuf) { + ret = statbuf->size == 0; + } + } + +done: + return ret; +} + +/** + * -flags test. + */ +bool eval_flags(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + if (!(statbuf->mask & BFS_STAT_ATTRS)) { + eval_error(state, "Couldn't get file %s.\n", bfs_stat_field_name(BFS_STAT_ATTRS)); + return false; + } + + unsigned long flags = statbuf->attrs; + unsigned long set = expr->set_flags; + unsigned long clear = expr->clear_flags; + + switch (expr->flags_cmp) { + case BFS_MODE_EQUAL: + return flags == set && !(flags & clear); + + case BFS_MODE_ALL: + return (flags & set) == set && !(flags & clear); + + case BFS_MODE_ANY: + return (flags & set) || (flags & clear) != clear; + } + + assert(!"Invalid comparison mode"); + return false; +} + +/** + * -fstype test. + */ +bool eval_fstype(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + const struct bfs_mtab *mtab = bfs_ctx_mtab(state->ctx); + if (!mtab) { + eval_report_error(state); + return false; + } + + const char *type = bfs_fstype(mtab, statbuf); + return strcmp(type, expr->argv[1]) == 0; +} + +/** + * -hidden test. + */ +bool eval_hidden(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + const char *name = ftwbuf->path + ftwbuf->nameoff; + + // Don't treat "." or ".." as hidden directories. Otherwise we'd filter + // out everything when given + // + // $ bfs . -nohidden + // $ bfs .. -nohidden + return name[0] == '.' && strcmp(name, ".") != 0 && strcmp(name, "..") != 0; +} + +/** + * -inum test. + */ +bool eval_inum(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + return bfs_expr_cmp(expr, statbuf->ino); +} + +/** + * -links test. + */ +bool eval_links(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + return bfs_expr_cmp(expr, statbuf->nlink); +} + +/** + * -i?lname test. + */ +bool eval_lname(const struct bfs_expr *expr, struct bfs_eval *state) { + bool ret = false; + char *name = NULL; + + const struct BFTW *ftwbuf = state->ftwbuf; + if (ftwbuf->type != BFS_LNK) { + goto done; + } + + const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW); + size_t len = statbuf ? statbuf->size : 0; + + name = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len); + if (!name) { + eval_report_error(state); + goto done; + } + + ret = fnmatch(expr->argv[1], name, expr->num) == 0; + +done: + free(name); + return ret; +} + +/** + * -i?name test. + */ +bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + + const char *name = ftwbuf->path + ftwbuf->nameoff; + char *copy = NULL; + if (ftwbuf->depth == 0) { + // Any trailing slashes are not part of the name. This can only + // happen for the root path. + const char *slash = strchr(name, '/'); + if (slash && slash > name) { + copy = strndup(name, slash - name); + if (!copy) { + eval_report_error(state); + return false; + } + name = copy; + } + } + + bool ret = fnmatch(expr->argv[1], name, expr->num) == 0; + free(copy); + return ret; +} + +/** + * -i?path test. + */ +bool eval_path(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + return fnmatch(expr->argv[1], ftwbuf->path, expr->num) == 0; +} + +/** + * -perm test. + */ +bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + mode_t mode = statbuf->mode; + mode_t target; + if (state->ftwbuf->type == BFS_DIR) { + target = expr->dir_mode; + } else { + target = expr->file_mode; + } + + switch (expr->mode_cmp) { + case BFS_MODE_EQUAL: + return (mode & 07777) == target; + + case BFS_MODE_ALL: + return (mode & target) == target; + + case BFS_MODE_ANY: + return !(mode & target) == !target; + } + + assert(!"Invalid comparison mode"); + return false; +} + +/** + * -f?ls action. + */ +bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { + CFILE *cfile = expr->cfile; + FILE *file = cfile->file; + const struct bfs_users *users = bfs_ctx_users(state->ctx); + const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); + const struct BFTW *ftwbuf = state->ftwbuf; + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + goto done; + } + + uintmax_t ino = statbuf->ino; + uintmax_t block_size = state->ctx->posixly_correct ? 512 : 1024; + uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + block_size - 1)/block_size; + char mode[11]; + xstrmode(statbuf->mode, mode); + char acl = bfs_check_acl(ftwbuf) > 0 ? '+' : ' '; + uintmax_t nlink = statbuf->nlink; + if (fprintf(file, "%9ju %6ju %s%c %2ju", ino, blocks, mode, acl, nlink) < 0) { + goto error; + } + + uintmax_t uid = statbuf->uid; + const struct passwd *pwd = users ? bfs_getpwuid(users, uid) : NULL; + if (pwd) { + if (fprintf(file, " %-8s", pwd->pw_name) < 0) { + goto error; + } + } else { + if (fprintf(file, " %-8ju", uid) < 0) { + goto error; + } + } + + uintmax_t gid = statbuf->gid; + const struct group *grp = groups ? bfs_getgrgid(groups, gid) : NULL; + if (grp) { + if (fprintf(file, " %-8s", grp->gr_name) < 0) { + goto error; + } + } else { + if (fprintf(file, " %-8ju", gid) < 0) { + goto error; + } + } + + if (ftwbuf->type == BFS_BLK || ftwbuf->type == BFS_CHR) { + int ma = bfs_major(statbuf->rdev); + int mi = bfs_minor(statbuf->rdev); + if (fprintf(file, " %3d, %3d", ma, mi) < 0) { + goto error; + } + } else { + uintmax_t size = statbuf->size; + if (fprintf(file, " %8ju", size) < 0) { + goto error; + } + } + + time_t time = statbuf->mtime.tv_sec; + time_t now = expr->reftime.tv_sec; + time_t six_months_ago = now - 6*30*24*60*60; + time_t tomorrow = now + 24*60*60; + struct tm tm; + if (xlocaltime(&time, &tm) != 0) { + goto error; + } + char time_str[256]; + const char *time_format = "%b %e %H:%M"; + if (time <= six_months_ago || time >= tomorrow) { + time_format = "%b %e %Y"; + } + if (!strftime(time_str, sizeof(time_str), time_format, &tm)) { + errno = EOVERFLOW; + goto error; + } + if (fprintf(file, " %s", time_str) < 0) { + goto error; + } + + if (cfprintf(cfile, " %pP", ftwbuf) < 0) { + goto error; + } + + if (ftwbuf->type == BFS_LNK) { + if (cfprintf(cfile, " -> %pL", ftwbuf) < 0) { + goto error; + } + } + + if (fputc('\n', file) == EOF) { + goto error; + } + +done: + return true; + +error: + eval_report_error(state); + return true; +} + +/** + * -f?print action. + */ +bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state) { + if (cfprintf(expr->cfile, "%pP\n", state->ftwbuf) < 0) { + eval_report_error(state); + } + return true; +} + +/** + * -f?print0 action. + */ +bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state) { + const char *path = state->ftwbuf->path; + size_t length = strlen(path) + 1; + if (fwrite(path, 1, length, expr->cfile->file) != length) { + eval_report_error(state); + } + return true; +} + +/** + * -f?printf action. + */ +bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state) { + if (bfs_printf(expr->cfile, expr->printf, state->ftwbuf) != 0) { + eval_report_error(state); + } + + return true; +} + +/** + * -printx action. + */ +bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) { + FILE *file = expr->cfile->file; + const char *path = state->ftwbuf->path; + + while (true) { + size_t span = strcspn(path, " \t\n\\$'\"`"); + if (fwrite(path, 1, span, file) != span) { + goto error; + } + path += span; + + char c = path[0]; + if (!c) { + break; + } + + char escaped[] = {'\\', c}; + if (fwrite(escaped, 1, sizeof(escaped), file) != sizeof(escaped)) { + goto error; + } + ++path; + } + + + if (fputc('\n', file) == EOF) { + goto error; + } + + return true; + +error: + eval_report_error(state); + return true; +} + +/** + * -prune action. + */ +bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state) { + state->action = BFTW_PRUNE; + return true; +} + +/** + * -quit action. + */ +bool eval_quit(const struct bfs_expr *expr, struct bfs_eval *state) { + state->action = BFTW_STOP; + state->quit = true; + return true; +} + +/** + * -i?regex test. + */ +bool eval_regex(const struct bfs_expr *expr, struct bfs_eval *state) { + const char *path = state->ftwbuf->path; + + int ret = bfs_regexec(expr->regex, path, BFS_REGEX_ANCHOR); + if (ret < 0) { + char *str = bfs_regerror(expr->regex); + if (str) { + eval_error(state, "%s.\n", str); + free(str); + } else { + eval_error(state, "bfs_regerror(): %m.\n"); + } + } + + return ret > 0; +} + +/** + * -samefile test. + */ +bool eval_samefile(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + return statbuf->dev == expr->dev && statbuf->ino == expr->ino; +} + +/** + * -size test. + */ +bool eval_size(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + static const off_t scales[] = { + [BFS_BLOCKS] = 512, + [BFS_BYTES] = 1, + [BFS_WORDS] = 2, + [BFS_KB] = 1LL << 10, + [BFS_MB] = 1LL << 20, + [BFS_GB] = 1LL << 30, + [BFS_TB] = 1LL << 40, + [BFS_PB] = 1LL << 50, + }; + + off_t scale = scales[expr->size_unit]; + off_t size = (statbuf->size + scale - 1)/scale; // Round up + return bfs_expr_cmp(expr, size); +} + +/** + * -sparse test. + */ +bool eval_sparse(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1)/BFS_STAT_BLKSIZE; + return statbuf->blocks < expected; +} + +/** + * -type test. + */ +bool eval_type(const struct bfs_expr *expr, struct bfs_eval *state) { + return (1 << state->ftwbuf->type) & expr->num; +} + +/** + * -xattr test. + */ +bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state) { + int ret = bfs_check_xattrs(state->ftwbuf); + if (ret >= 0) { + return ret; + } else { + eval_report_error(state); + return false; + } +} + +/** + * -xattrname test. + */ +bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state) { + int ret = bfs_check_xattr_named(state->ftwbuf, expr->argv[1]); + if (ret >= 0) { + return ret; + } else { + eval_report_error(state); + return false; + } +} + +/** + * -xtype test. + */ +bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) { + const struct BFTW *ftwbuf = state->ftwbuf; + enum bfs_stat_flags flags = ftwbuf->stat_flags ^ (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW); + enum bfs_type type = bftw_type(ftwbuf, flags); + if (type == BFS_ERROR) { + eval_report_error(state); + return false; + } else { + return (1 << type) & expr->num; + } +} + +#if _POSIX_MONOTONIC_CLOCK > 0 +# define BFS_CLOCK CLOCK_MONOTONIC +#elif _POSIX_TIMERS > 0 +# define BFS_CLOCK CLOCK_REALTIME +#endif + +/** + * Call clock_gettime(), if available. + */ +static int eval_gettime(struct bfs_eval *state, struct timespec *ts) { +#ifdef BFS_CLOCK + int ret = clock_gettime(BFS_CLOCK, ts); + if (ret != 0) { + bfs_warning(state->ctx, "%pP: clock_gettime(): %m.\n", state->ftwbuf); + } + return ret; +#else + return -1; +#endif +} + +/** + * Record an elapsed time. + */ +static void timespec_elapsed(struct timespec *elapsed, const struct timespec *start, const struct timespec *end) { + elapsed->tv_sec += end->tv_sec - start->tv_sec; + elapsed->tv_nsec += end->tv_nsec - start->tv_nsec; + if (elapsed->tv_nsec < 0) { + elapsed->tv_nsec += 1000000000L; + --elapsed->tv_sec; + } else if (elapsed->tv_nsec >= 1000000000L) { + elapsed->tv_nsec -= 1000000000L; + ++elapsed->tv_sec; + } +} + +/** + * Evaluate an expression. + */ +static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) { + struct timespec start, end; + bool time = state->ctx->debug & DEBUG_RATES; + if (time) { + if (eval_gettime(state, &start) != 0) { + time = false; + } + } + + assert(!state->quit); + + bool ret = expr->eval_fn(expr, state); + + if (time) { + if (eval_gettime(state, &end) == 0) { + timespec_elapsed(&expr->elapsed, &start, &end); + } + } + + ++expr->evaluations; + if (ret) { + ++expr->successes; + } + + if (bfs_expr_never_returns(expr)) { + assert(state->quit); + } else if (!state->quit) { + assert(!expr->always_true || ret); + assert(!expr->always_false || !ret); + } + + return ret; +} + +/** + * Evaluate a negation. + */ +bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state) { + return !eval_expr(expr->rhs, state); +} + +/** + * Evaluate a conjunction. + */ +bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) { + if (!eval_expr(expr->lhs, state)) { + return false; + } + + if (state->quit) { + return false; + } + + return eval_expr(expr->rhs, state); +} + +/** + * Evaluate a disjunction. + */ +bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) { + if (eval_expr(expr->lhs, state)) { + return true; + } + + if (state->quit) { + return false; + } + + return eval_expr(expr->rhs, state); +} + +/** + * Evaluate the comma operator. + */ +bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) { + eval_expr(expr->lhs, state); + + if (state->quit) { + return false; + } + + return eval_expr(expr->rhs, state); +} + +/** Update the status bar. */ +static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct timespec *last_status, size_t count) { + struct timespec now; + if (eval_gettime(state, &now) == 0) { + struct timespec elapsed = {0}; + timespec_elapsed(&elapsed, last_status, &now); + + // Update every 0.1s + if (elapsed.tv_sec > 0 || elapsed.tv_nsec >= 100000000L) { + *last_status = now; + } else { + return; + } + } + + size_t width = bfs_bar_width(bar); + if (width < 3) { + return; + } + + const struct BFTW *ftwbuf = state->ftwbuf; + + char *rhs = dstrprintf(" (visited: %zu, depth: %2zu)", count, ftwbuf->depth); + if (!rhs) { + return; + } + + size_t rhslen = dstrlen(rhs); + if (3 + rhslen > width) { + dstresize(&rhs, 0); + rhslen = 0; + } + + char *status = dstralloc(0); + if (!status) { + goto out_rhs; + } + + const char *path = ftwbuf->path; + size_t pathlen = ftwbuf->nameoff; + if (ftwbuf->depth == 0) { + pathlen = strlen(path); + } + + // Try to make sure even wide characters fit in the status bar + size_t pathmax = width - rhslen - 3; + size_t pathwidth = 0; + mbstate_t mb; + memset(&mb, 0, sizeof(mb)); + while (pathlen > 0) { + wchar_t wc; + size_t len = mbrtowc(&wc, path, pathlen, &mb); + int cwidth; + if (len == (size_t)-1) { + // Invalid byte sequence, assume a single-width '?' + len = 1; + cwidth = 1; + memset(&mb, 0, sizeof(mb)); + } else if (len == (size_t)-2) { + // Incomplete byte sequence, assume a single-width '?' + len = pathlen; + cwidth = 1; + } else { + cwidth = wcwidth(wc); + if (cwidth < 0) { + cwidth = 0; + } + } + + if (pathwidth + cwidth > pathmax) { + break; + } + + if (dstrncat(&status, path, len) != 0) { + goto out_rhs; + } + + path += len; + pathlen -= len; + pathwidth += cwidth; + } + + if (dstrcat(&status, "...") != 0) { + goto out_rhs; + } + + while (pathwidth < pathmax) { + if (dstrapp(&status, ' ') != 0) { + goto out_rhs; + } + ++pathwidth; + } + + if (dstrcat(&status, rhs) != 0) { + goto out_rhs; + } + + bfs_bar_update(bar, status); + + dstrfree(status); +out_rhs: + dstrfree(rhs); +} + +/** Check if we've seen a file before. */ +static bool eval_file_unique(struct bfs_eval *state, struct trie *seen) { + const struct bfs_stat *statbuf = eval_stat(state); + if (!statbuf) { + return false; + } + + bfs_file_id id; + bfs_stat_id(statbuf, &id); + + struct trie_leaf *leaf = trie_insert_mem(seen, id, sizeof(id)); + if (!leaf) { + eval_report_error(state); + return false; + } + + if (leaf->value) { + state->action = BFTW_PRUNE; + return false; + } else { + leaf->value = leaf; + return true; + } +} + +#define DEBUG_FLAG(flags, flag) \ + do { \ + if ((flags & flag) || flags == flag) { \ + fputs(#flag, stderr); \ + flags ^= flag; \ + if (flags) { \ + fputs(" | ", stderr); \ + } \ + } \ + } while (0) + +/** + * Log a stat() call. + */ +static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, const struct bftw_stat *cache, enum bfs_stat_flags flags) { + bfs_debug_prefix(ctx, DEBUG_STAT); + + fprintf(stderr, "bfs_stat("); + if (ftwbuf->at_fd == AT_FDCWD) { + fprintf(stderr, "AT_FDCWD"); + } else { + size_t baselen = strlen(ftwbuf->path) - strlen(ftwbuf->at_path); + fprintf(stderr, "\""); + fwrite(ftwbuf->path, 1, baselen, stderr); + fprintf(stderr, "\""); + } + + fprintf(stderr, ", \"%s\", ", ftwbuf->at_path); + + DEBUG_FLAG(flags, BFS_STAT_FOLLOW); + DEBUG_FLAG(flags, BFS_STAT_NOFOLLOW); + DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW); + + fprintf(stderr, ") == %d", cache->buf ? 0 : -1); + + if (cache->error) { + fprintf(stderr, " [%d]", cache->error); + } + + fprintf(stderr, "\n"); +} + +/** + * Log any stat() calls that happened. + */ +static void debug_stats(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf) { + if (!(ctx->debug & DEBUG_STAT)) { + return; + } + + const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf; + if (statbuf || ftwbuf->stat_cache.error) { + debug_stat(ctx, ftwbuf, &ftwbuf->stat_cache, BFS_STAT_FOLLOW); + } + + const struct bfs_stat *lstatbuf = ftwbuf->lstat_cache.buf; + if ((lstatbuf && lstatbuf != statbuf) || ftwbuf->lstat_cache.error) { + debug_stat(ctx, ftwbuf, &ftwbuf->lstat_cache, BFS_STAT_NOFOLLOW); + } +} + +#define DUMP_MAP(value) [value] = #value + +/** + * Dump the bfs_type for -D search. + */ +static const char *dump_bfs_type(enum bfs_type type) { + static const char *types[] = { + DUMP_MAP(BFS_UNKNOWN), + DUMP_MAP(BFS_BLK), + DUMP_MAP(BFS_CHR), + DUMP_MAP(BFS_DIR), + DUMP_MAP(BFS_DOOR), + DUMP_MAP(BFS_FIFO), + DUMP_MAP(BFS_LNK), + DUMP_MAP(BFS_PORT), + DUMP_MAP(BFS_REG), + DUMP_MAP(BFS_SOCK), + DUMP_MAP(BFS_WHT), + }; + + if (type == BFS_ERROR) { + return "BFS_ERROR"; + } else { + return types[type]; + } +} + +/** + * Dump the bftw_visit for -D search. + */ +static const char *dump_bftw_visit(enum bftw_visit visit) { + static const char *visits[] = { + DUMP_MAP(BFTW_PRE), + DUMP_MAP(BFTW_POST), + }; + return visits[visit]; +} + +/** + * Dump the bftw_action for -D search. + */ +static const char *dump_bftw_action(enum bftw_action action) { + static const char *actions[] = { + DUMP_MAP(BFTW_CONTINUE), + DUMP_MAP(BFTW_PRUNE), + DUMP_MAP(BFTW_STOP), + }; + return actions[action]; +} + +/** + * Type passed as the argument to the bftw() callback. + */ +struct callback_args { + /** The bfs context. */ + const struct bfs_ctx *ctx; + + /** The status bar. */ + struct bfs_bar *bar; + /** The time of the last status update. */ + struct timespec last_status; + /** The number of files visited so far. */ + size_t count; + + /** The set of seen files. */ + struct trie *seen; + + /** Eventual return value from bfs_eval(). */ + int ret; +}; + +/** + * bftw() callback. + */ +static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) { + struct callback_args *args = ptr; + ++args->count; + + const struct bfs_ctx *ctx = args->ctx; + + struct bfs_eval state; + state.ftwbuf = ftwbuf; + state.ctx = ctx; + state.action = BFTW_CONTINUE; + state.ret = &args->ret; + state.quit = false; + + if (args->bar) { + eval_status(&state, args->bar, &args->last_status, args->count); + } + + if (ftwbuf->type == BFS_ERROR) { + if (!eval_should_ignore(&state, ftwbuf->error)) { + eval_error(&state, "%s.\n", strerror(ftwbuf->error)); + } + state.action = BFTW_PRUNE; + goto done; + } + + if (ctx->unique && ftwbuf->visit == BFTW_PRE) { + if (!eval_file_unique(&state, args->seen)) { + goto done; + } + } + + if (eval_expr(ctx->exclude, &state)) { + state.action = BFTW_PRUNE; + goto done; + } + + if (ctx->xargs_safe && strpbrk(ftwbuf->path, " \t\n\'\"\\")) { + eval_error(&state, "Path is not safe for xargs.\n"); + state.action = BFTW_PRUNE; + goto done; + } + + if (ctx->maxdepth < 0 || ftwbuf->depth >= (size_t)ctx->maxdepth) { + state.action = BFTW_PRUNE; + } + + // In -depth mode, only handle directories on the BFTW_POST visit + enum bftw_visit expected_visit = BFTW_PRE; + if ((ctx->flags & BFTW_POST_ORDER) + && (ctx->strategy == BFTW_IDS || ftwbuf->type == BFS_DIR) + && ftwbuf->depth < (size_t)ctx->maxdepth) { + expected_visit = BFTW_POST; + } + + if (ftwbuf->visit == expected_visit + && ftwbuf->depth >= (size_t)ctx->mindepth + && ftwbuf->depth <= (size_t)ctx->maxdepth) { + eval_expr(ctx->expr, &state); + } + +done: + debug_stats(ctx, ftwbuf); + + if (bfs_debug(ctx, DEBUG_SEARCH, "eval_callback({\n")) { + fprintf(stderr, "\t.path = \"%s\",\n", ftwbuf->path); + fprintf(stderr, "\t.root = \"%s\",\n", ftwbuf->root); + fprintf(stderr, "\t.depth = %zu,\n", ftwbuf->depth); + fprintf(stderr, "\t.visit = %s,\n", dump_bftw_visit(ftwbuf->visit)); + fprintf(stderr, "\t.type = %s,\n", dump_bfs_type(ftwbuf->type)); + fprintf(stderr, "\t.error = %d,\n", ftwbuf->error); + fprintf(stderr, "}) == %s\n", dump_bftw_action(state.action)); + } + + return state.action; +} + +/** Check if an rlimit value is infinite. */ +static bool rlim_isinf(rlim_t r) { + // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite + if (r == RLIM_INFINITY) { + return true; + } + +#ifdef RLIM_SAVED_CUR + if (r == RLIM_SAVED_CUR) { + return true; + } +#endif + +#ifdef RLIM_SAVED_MAX + if (r == RLIM_SAVED_MAX) { + return true; + } +#endif + + return false; +} + +/** Compare two rlimit values, accounting for RLIM_INFINITY etc. */ +static int rlim_cmp(rlim_t a, rlim_t b) { + bool a_inf = rlim_isinf(a); + bool b_inf = rlim_isinf(b); + if (a_inf || b_inf) { + return a_inf - b_inf; + } + + return (a > b) - (a < b); +} + +/** Raise RLIMIT_NOFILE if possible, and return the new limit. */ +static int raise_fdlimit(const struct bfs_ctx *ctx) { + rlim_t target = 64 << 10; + if (rlim_cmp(target, ctx->nofile_hard) > 0) { + target = ctx->nofile_hard; + } + + int ret = target; + + if (rlim_cmp(target, ctx->nofile_soft) > 0) { + const struct rlimit rl = { + .rlim_cur = target, + .rlim_max = ctx->nofile_hard, + }; + if (setrlimit(RLIMIT_NOFILE, &rl) != 0) { + ret = ctx->nofile_soft; + } + } + + return ret; +} + +/** Infer the number of file descriptors available to bftw(). */ +static int infer_fdlimit(const struct bfs_ctx *ctx, int limit) { + // 3 for std{in,out,err} + int nopen = 3 + ctx->nfiles; + + // Check /proc/self/fd for the current number of open fds, if possible + // (we may have inherited more than just the standard ones) + struct bfs_dir *dir = bfs_opendir(AT_FDCWD, "/proc/self/fd"); + if (!dir) { + dir = bfs_opendir(AT_FDCWD, "/dev/fd"); + } + if (dir) { + // Account for 'dir' itself + nopen = -1; + + while (bfs_readdir(dir, NULL) > 0) { + ++nopen; + } + + bfs_closedir(dir); + } + + int ret = limit - nopen; + ret -= ctx->expr->persistent_fds; + ret -= ctx->expr->ephemeral_fds; + + // bftw() needs at least 2 available fds + if (ret < 2) { + ret = 2; + } + + return ret; +} + +/** + * Dump the bftw() flags for -D search. + */ +static void dump_bftw_flags(enum bftw_flags flags) { + DEBUG_FLAG(flags, 0); + DEBUG_FLAG(flags, BFTW_STAT); + DEBUG_FLAG(flags, BFTW_RECOVER); + DEBUG_FLAG(flags, BFTW_POST_ORDER); + DEBUG_FLAG(flags, BFTW_FOLLOW_ROOTS); + DEBUG_FLAG(flags, BFTW_FOLLOW_ALL); + DEBUG_FLAG(flags, BFTW_DETECT_CYCLES); + DEBUG_FLAG(flags, BFTW_SKIP_MOUNTS); + DEBUG_FLAG(flags, BFTW_PRUNE_MOUNTS); + DEBUG_FLAG(flags, BFTW_SORT); + DEBUG_FLAG(flags, BFTW_BUFFER); + + assert(!flags); +} + +/** + * Dump the bftw_strategy for -D search. + */ +static const char *dump_bftw_strategy(enum bftw_strategy strategy) { + static const char *strategies[] = { + DUMP_MAP(BFTW_BFS), + DUMP_MAP(BFTW_DFS), + DUMP_MAP(BFTW_IDS), + DUMP_MAP(BFTW_EDS), + }; + return strategies[strategy]; +} + +/** Check if we need to enable BFTW_BUFFER. */ +static bool eval_must_buffer(const struct bfs_expr *expr) { +#if __FreeBSD__ + // FreeBSD doesn't properly handle adding/removing directory entries + // during readdir() on NFS mounts. Work around it by passing BFTW_BUFFER + // whenever we could be mutating the directory ourselves through -delete + // or -exec. We don't attempt to handle concurrent modification by other + // processes, which are racey anyway. + // + // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=57696 + // https://github.com/tavianator/bfs/issues/67 + + if (expr->eval_fn == eval_delete || expr->eval_fn == eval_exec) { + return true; + } + + if (bfs_expr_has_children(expr)) { + if (expr->lhs && eval_must_buffer(expr->lhs)) { + return true; + } + + if (expr->rhs && eval_must_buffer(expr->rhs)) { + return true; + } + } +#endif // __FreeBSD__ + + return false; +} + +int bfs_eval(const struct bfs_ctx *ctx) { + if (!ctx->expr) { + return EXIT_SUCCESS; + } + + struct callback_args args = { + .ctx = ctx, + .ret = EXIT_SUCCESS, + }; + + if (ctx->status) { + args.bar = bfs_bar_show(); + if (!args.bar) { + bfs_warning(ctx, "Couldn't show status bar: %m.\n\n"); + } + } + + struct trie seen; + if (ctx->unique) { + trie_init(&seen); + args.seen = &seen; + } + + int fdlimit = raise_fdlimit(ctx); + fdlimit = infer_fdlimit(ctx, fdlimit); + + struct bftw_args bftw_args = { + .paths = ctx->paths, + .npaths = darray_length(ctx->paths), + .callback = eval_callback, + .ptr = &args, + .nopenfd = fdlimit, + .flags = ctx->flags, + .strategy = ctx->strategy, + .mtab = bfs_ctx_mtab(ctx), + }; + + if (eval_must_buffer(ctx->expr)) { + bftw_args.flags |= BFTW_BUFFER; + } + + if (bfs_debug(ctx, DEBUG_SEARCH, "bftw({\n")) { + fprintf(stderr, "\t.paths = {\n"); + for (size_t i = 0; i < bftw_args.npaths; ++i) { + fprintf(stderr, "\t\t\"%s\",\n", bftw_args.paths[i]); + } + fprintf(stderr, "\t},\n"); + fprintf(stderr, "\t.npaths = %zu,\n", bftw_args.npaths); + fprintf(stderr, "\t.callback = eval_callback,\n"); + fprintf(stderr, "\t.ptr = &args,\n"); + fprintf(stderr, "\t.nopenfd = %d,\n", bftw_args.nopenfd); + fprintf(stderr, "\t.flags = "); + dump_bftw_flags(bftw_args.flags); + fprintf(stderr, ",\n\t.strategy = %s,\n", dump_bftw_strategy(bftw_args.strategy)); + fprintf(stderr, "\t.mtab = "); + if (bftw_args.mtab) { + fprintf(stderr, "ctx->mtab"); + } else { + fprintf(stderr, "NULL"); + } + fprintf(stderr, ",\n})\n"); + } + + if (bftw(&bftw_args) != 0) { + args.ret = EXIT_FAILURE; + bfs_perror(ctx, "bftw()"); + } + + if (eval_exec_finish(ctx->expr, ctx) != 0) { + args.ret = EXIT_FAILURE; + } + + bfs_ctx_dump(ctx, DEBUG_RATES); + + if (ctx->unique) { + trie_destroy(&seen); + } + + bfs_bar_hide(args.bar); + + return args.ret; +} |