diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bar.c | 248 | ||||
-rw-r--r-- | src/bar.h | 57 | ||||
-rw-r--r-- | src/bfs.h | 32 | ||||
-rw-r--r-- | src/bftw.c | 1494 | ||||
-rw-r--r-- | src/bftw.h | 223 | ||||
-rw-r--r-- | src/color.c | 1125 | ||||
-rw-r--r-- | src/color.h | 120 | ||||
-rw-r--r-- | src/ctx.c | 311 | ||||
-rw-r--r-- | src/ctx.h | 212 | ||||
-rw-r--r-- | src/darray.c | 103 | ||||
-rw-r--r-- | src/darray.h | 110 | ||||
-rw-r--r-- | src/diag.c | 233 | ||||
-rw-r--r-- | src/diag.h | 108 | ||||
-rw-r--r-- | src/dir.c | 303 | ||||
-rw-r--r-- | src/dir.h | 124 | ||||
-rw-r--r-- | src/dstring.c | 220 | ||||
-rw-r--r-- | src/dstring.h | 194 | ||||
-rw-r--r-- | src/eval.c | 1644 | ||||
-rw-r--r-- | src/eval.h | 113 | ||||
-rw-r--r-- | src/exec.c | 715 | ||||
-rw-r--r-- | src/exec.h | 121 | ||||
-rw-r--r-- | src/expr.h | 235 | ||||
-rw-r--r-- | src/fsade.c | 392 | ||||
-rw-r--r-- | src/fsade.h | 83 | ||||
-rw-r--r-- | src/main.c | 141 | ||||
-rw-r--r-- | src/mtab.c | 246 | ||||
-rw-r--r-- | src/mtab.h | 71 | ||||
-rw-r--r-- | src/opt.c | 1088 | ||||
-rw-r--r-- | src/opt.h | 37 | ||||
-rw-r--r-- | src/parse.c | 3959 | ||||
-rw-r--r-- | src/parse.h | 36 | ||||
-rw-r--r-- | src/printf.c | 927 | ||||
-rw-r--r-- | src/printf.h | 68 | ||||
-rw-r--r-- | src/pwcache.c | 293 | ||||
-rw-r--r-- | src/pwcache.h | 117 | ||||
-rw-r--r-- | src/stat.c | 376 | ||||
-rw-r--r-- | src/stat.h | 155 | ||||
-rw-r--r-- | src/trie.c | 693 | ||||
-rw-r--r-- | src/trie.h | 156 | ||||
-rw-r--r-- | src/typo.c | 176 | ||||
-rw-r--r-- | src/typo.h | 31 | ||||
-rw-r--r-- | src/util.c | 510 | ||||
-rw-r--r-- | src/util.h | 317 | ||||
-rw-r--r-- | src/xregex.c | 301 | ||||
-rw-r--r-- | src/xregex.h | 97 | ||||
-rw-r--r-- | src/xspawn.c | 318 | ||||
-rw-r--r-- | src/xspawn.h | 123 | ||||
-rw-r--r-- | src/xtime.c | 323 | ||||
-rw-r--r-- | src/xtime.h | 86 |
49 files changed, 19165 insertions, 0 deletions
diff --git a/src/bar.c b/src/bar.c new file mode 100644 index 0000000..b0e595e --- /dev/null +++ b/src/bar.c @@ -0,0 +1,248 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020-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. * + ****************************************************************************/ + +#include "bar.h" +#include "dstring.h" +#include "util.h" +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <sys/ioctl.h> +#include <unistd.h> + +struct bfs_bar { + int fd; + volatile sig_atomic_t width; + volatile sig_atomic_t height; +}; + +/** The global status bar instance. */ +static struct bfs_bar the_bar = { + .fd = -1, +}; + +/** Get the terminal size, if possible. */ +static int bfs_bar_getsize(struct bfs_bar *bar) { +#ifdef TIOCGWINSZ + struct winsize ws; + if (ioctl(bar->fd, TIOCGWINSZ, &ws) != 0) { + return -1; + } + + bar->width = ws.ws_col; + bar->height = ws.ws_row; + return 0; +#else + errno = ENOTSUP; + return -1; +#endif +} + +/** Async Signal Safe puts(). */ +static int ass_puts(int fd, const char *str) { + size_t len = strlen(str); + return xwrite(fd, str, len) == len ? 0 : -1; +} + +/** Number of decimal digits needed for terminal sizes. */ +#define ITOA_DIGITS ((sizeof(unsigned short) * CHAR_BIT + 2) / 3) + +/** Async Signal Safe itoa(). */ +static char *ass_itoa(char *str, unsigned int n) { + char *end = str + ITOA_DIGITS; + *end = '\0'; + + char *c = end; + do { + *--c = '0' + (n % 10); + n /= 10; + } while (n); + + size_t len = end - c; + memmove(str, c, len + 1); + return str + len; +} + +/** Update the size of the scrollable region. */ +static int bfs_bar_resize(struct bfs_bar *bar) { + char esc_seq[12 + ITOA_DIGITS] = + "\0337" // DECSC: Save cursor + "\033[;"; // DECSTBM: Set scrollable region + + // DECSTBM takes the height as the second argument + char *ptr = esc_seq + strlen(esc_seq); + ptr = ass_itoa(ptr, bar->height - 1); + + strcpy(ptr, + "r" // DECSTBM + "\0338" // DECRC: Restore the cursor + "\033[J" // ED: Erase display from cursor to end + ); + + return ass_puts(bar->fd, esc_seq); +} + +#ifdef SIGWINCH +/** SIGWINCH handler. */ +static void sighand_winch(int sig) { + int error = errno; + + bfs_bar_getsize(&the_bar); + bfs_bar_resize(&the_bar); + + errno = error; +} +#endif + +/** Reset the scrollable region and hide the bar. */ +static int bfs_bar_reset(struct bfs_bar *bar) { + return ass_puts(bar->fd, + "\0337" // DECSC: Save cursor + "\033[r" // DECSTBM: Reset scrollable region + "\0338" // DECRC: Restore cursor + "\033[J" // ED: Erase display from cursor to end + ); +} + +/** Signal handler for process-terminating signals. */ +static void sighand_reset(int sig) { + bfs_bar_reset(&the_bar); + raise(sig); +} + +/** Register sighand_reset() for a signal. */ +static void reset_before_death_by(int sig) { + struct sigaction sa = { + .sa_handler = sighand_reset, + .sa_flags = SA_RESETHAND, + }; + sigemptyset(&sa.sa_mask); + sigaction(sig, &sa, NULL); +} + +/** printf() to the status bar with a single write(). */ +BFS_FORMATTER(2, 3) +static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) { + va_list args; + va_start(args, format); + char *str = dstrvprintf(format, args); + va_end(args); + + if (!str) { + return -1; + } + + int ret = ass_puts(bar->fd, str); + dstrfree(str); + return ret; +} + +struct bfs_bar *bfs_bar_show(void) { + if (the_bar.fd >= 0) { + errno = EBUSY; + goto fail; + } + + char term[L_ctermid]; + ctermid(term); + if (strlen(term) == 0) { + errno = ENOTTY; + goto fail; + } + + the_bar.fd = open(term, O_RDWR | O_CLOEXEC); + if (the_bar.fd < 0) { + goto fail; + } + + if (bfs_bar_getsize(&the_bar) != 0) { + goto fail_close; + } + + reset_before_death_by(SIGABRT); + reset_before_death_by(SIGINT); + reset_before_death_by(SIGPIPE); + reset_before_death_by(SIGQUIT); + reset_before_death_by(SIGTERM); + +#ifdef SIGWINCH + struct sigaction sa = { + .sa_handler = sighand_winch, + .sa_flags = SA_RESTART, + }; + sigemptyset(&sa.sa_mask); + sigaction(SIGWINCH, &sa, NULL); +#endif + + bfs_bar_printf(&the_bar, + "\n" // Make space for the bar + "\0337" // DECSC: Save cursor + "\033[;%ur" // DECSTBM: Set scrollable region + "\0338" // DECRC: Restore cursor + "\033[1A", // CUU: Move cursor up 1 row + (unsigned int)(the_bar.height - 1) + ); + + return &the_bar; + +fail_close: + close_quietly(the_bar.fd); + the_bar.fd = -1; +fail: + return NULL; +} + +unsigned int bfs_bar_width(const struct bfs_bar *bar) { + return bar->width; +} + +int bfs_bar_update(struct bfs_bar *bar, const char *str) { + return bfs_bar_printf(bar, + "\0337" // DECSC: Save cursor + "\033[%u;0f" // HVP: Move cursor to row, column + "\033[K" // EL: Erase line + "\033[7m" // SGR reverse video + "%s" + "\033[27m" // SGR reverse video off + "\0338", // DECRC: Restore cursor + (unsigned int)bar->height, + str + ); +} + +void bfs_bar_hide(struct bfs_bar *bar) { + if (!bar) { + return; + } + + signal(SIGABRT, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGPIPE, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); +#ifdef SIGWINCH + signal(SIGWINCH, SIG_DFL); +#endif + + bfs_bar_reset(bar); + + xclose(bar->fd); + bar->fd = -1; +} diff --git a/src/bar.h b/src/bar.h new file mode 100644 index 0000000..3e509d6 --- /dev/null +++ b/src/bar.h @@ -0,0 +1,57 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020 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. * + ****************************************************************************/ + +/** + * A terminal status bar. + */ + +#ifndef BFS_BAR_H +#define BFS_BAR_H + +/** A terminal status bar. */ +struct bfs_bar; + +/** + * Create a terminal status bar. Only one status bar is supported at a time. + * + * @return + * A pointer to the new status bar, or NULL on failure. + */ +struct bfs_bar *bfs_bar_show(void); + +/** + * Get the width of the status bar. + */ +unsigned int bfs_bar_width(const struct bfs_bar *bar); + +/** + * Update the status bar message. + * + * @param bar + * The status bar to update. + * @param str + * The string to display. + * @return + * 0 on success, -1 on failure. + */ +int bfs_bar_update(struct bfs_bar *bar, const char *str); + +/** + * Hide the status bar. + */ +void bfs_bar_hide(struct bfs_bar *status); + +#endif // BFS_BAR_H diff --git a/src/bfs.h b/src/bfs.h new file mode 100644 index 0000000..93d8d79 --- /dev/null +++ b/src/bfs.h @@ -0,0 +1,32 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2015-2021 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. * + ****************************************************************************/ + +/** + * Constants about the bfs program itself. + */ + +#ifndef BFS_H +#define BFS_H + +#ifndef BFS_VERSION +# define BFS_VERSION "2.5" +#endif + +#ifndef BFS_HOMEPAGE +# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html" +#endif + +#endif // BFS_H diff --git a/src/bftw.c b/src/bftw.c new file mode 100644 index 0000000..6f97bf6 --- /dev/null +++ b/src/bftw.c @@ -0,0 +1,1494 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +/** + * The bftw() implementation consists of the following components: + * + * - struct bftw_file: A file that has been encountered during the traversal. + * They have reference-counted links to their parents in the directory tree. + * + * - struct bftw_cache: An LRU list of bftw_file's with open file descriptors, + * used for openat() to minimize the amount of path re-traversals. + * + * - struct bftw_queue: The queue of bftw_file's left to explore. Implemented + * as a simple circular buffer. + * + * - struct bftw_state: Represents the current state of the traversal, allowing + * various helper functions to take fewer parameters. + */ + +#include "bftw.h" +#include "dir.h" +#include "darray.h" +#include "dstring.h" +#include "mtab.h" +#include "stat.h" +#include "trie.h" +#include "util.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +/** + * A file. + */ +struct bftw_file { + /** The parent directory, if any. */ + struct bftw_file *parent; + /** The root under which this file was found. */ + struct bftw_file *root; + /** The next file in the queue, if any. */ + struct bftw_file *next; + + /** The previous file in the LRU list. */ + struct bftw_file *lru_prev; + /** The next file in the LRU list. */ + struct bftw_file *lru_next; + + /** This file's depth in the walk. */ + size_t depth; + /** Reference count. */ + size_t refcount; + + /** An open descriptor to this file, or -1. */ + int fd; + + /** This file's type, if known. */ + enum bfs_type type; + /** The device number, for cycle detection. */ + dev_t dev; + /** The inode number, for cycle detection. */ + ino_t ino; + + /** The offset of this file in the full path. */ + size_t nameoff; + /** The length of the file's name. */ + size_t namelen; + /** The file's name. */ + char name[]; +}; + +/** + * A cache of open directories. + */ +struct bftw_cache { + /** The head of the LRU list. */ + struct bftw_file *head; + /** The insertion target for the LRU list. */ + struct bftw_file *target; + /** The tail of the LRU list. */ + struct bftw_file *tail; + /** The remaining capacity of the LRU list. */ + size_t capacity; +}; + +/** Initialize a cache. */ +static void bftw_cache_init(struct bftw_cache *cache, size_t capacity) { + cache->head = NULL; + cache->target = NULL; + cache->tail = NULL; + cache->capacity = capacity; +} + +/** Destroy a cache. */ +static void bftw_cache_destroy(struct bftw_cache *cache) { + assert(!cache->tail); + assert(!cache->target); + assert(!cache->head); +} + +/** Add a bftw_file to the cache. */ +static void bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) { + assert(cache->capacity > 0); + assert(file->fd >= 0); + assert(!file->lru_prev); + assert(!file->lru_next); + + if (cache->target) { + file->lru_prev = cache->target; + file->lru_next = cache->target->lru_next; + } else { + file->lru_next = cache->head; + } + + if (file->lru_prev) { + file->lru_prev->lru_next = file; + } else { + cache->head = file; + } + + if (file->lru_next) { + file->lru_next->lru_prev = file; + } else { + cache->tail = file; + } + + // Prefer to keep the root paths open by keeping them at the head of the list + if (file->depth == 0) { + cache->target = file; + } + + --cache->capacity; +} + +/** Remove a bftw_file from the cache. */ +static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) { + if (cache->target == file) { + cache->target = file->lru_prev; + } + + if (file->lru_prev) { + assert(cache->head != file); + file->lru_prev->lru_next = file->lru_next; + } else { + assert(cache->head == file); + cache->head = file->lru_next; + } + + if (file->lru_next) { + assert(cache->tail != file); + file->lru_next->lru_prev = file->lru_prev; + } else { + assert(cache->tail == file); + cache->tail = file->lru_prev; + } + + file->lru_prev = NULL; + file->lru_next = NULL; + ++cache->capacity; +} + +/** Mark a cache entry as recently used. */ +static void bftw_cache_use(struct bftw_cache *cache, struct bftw_file *file) { + bftw_cache_remove(cache, file); + bftw_cache_add(cache, file); +} + +/** Close a bftw_file. */ +static void bftw_file_close(struct bftw_cache *cache, struct bftw_file *file) { + assert(file->fd >= 0); + + bftw_cache_remove(cache, file); + + xclose(file->fd); + file->fd = -1; +} + +/** Pop a directory from the cache. */ +static void bftw_cache_pop(struct bftw_cache *cache) { + assert(cache->tail); + bftw_file_close(cache, cache->tail); +} + +/** + * Shrink the cache, to recover from EMFILE. + * + * @param cache + * The cache in question. + * @param saved + * A bftw_file that must be preserved. + * @return + * 0 if successfully shrunk, otherwise -1. + */ +static int bftw_cache_shrink(struct bftw_cache *cache, const struct bftw_file *saved) { + struct bftw_file *file = cache->tail; + if (!file) { + return -1; + } + + if (file == saved) { + file = file->lru_prev; + if (!file) { + return -1; + } + } + + bftw_file_close(cache, file); + cache->capacity = 0; + return 0; +} + +/** Compute the name offset of a child path. */ +static size_t bftw_child_nameoff(const struct bftw_file *parent) { + size_t ret = parent->nameoff + parent->namelen; + if (parent->name[parent->namelen - 1] != '/') { + ++ret; + } + return ret; +} + +/** Create a new bftw_file. */ +static struct bftw_file *bftw_file_new(struct bftw_file *parent, const char *name) { + size_t namelen = strlen(name); + size_t size = BFS_FLEX_SIZEOF(struct bftw_file, name, namelen + 1); + + struct bftw_file *file = malloc(size); + if (!file) { + return NULL; + } + + file->parent = parent; + + if (parent) { + file->root = parent->root; + file->depth = parent->depth + 1; + file->nameoff = bftw_child_nameoff(parent); + ++parent->refcount; + } else { + file->root = file; + file->depth = 0; + file->nameoff = 0; + } + + file->next = NULL; + + file->lru_prev = NULL; + file->lru_next = NULL; + + file->refcount = 1; + file->fd = -1; + + file->type = BFS_UNKNOWN; + file->dev = -1; + file->ino = -1; + + file->namelen = namelen; + memcpy(file->name, name, namelen + 1); + + return file; +} + +/** + * Open a bftw_file relative to another one. + * + * @param cache + * The cache to hold the file. + * @param file + * The file to open. + * @param base + * The base directory for the relative path (may be NULL). + * @param at_fd + * The base file descriptor, AT_FDCWD if base == NULL. + * @param at_path + * The relative path to the file. + * @return + * The opened file descriptor, or negative on error. + */ +static int bftw_file_openat(struct bftw_cache *cache, struct bftw_file *file, struct bftw_file *base, const char *at_path) { + assert(file->fd < 0); + + int at_fd = AT_FDCWD; + if (base) { + bftw_cache_use(cache, base); + at_fd = base->fd; + } + + int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY; + int fd = openat(at_fd, at_path, flags); + + if (fd < 0 && errno == EMFILE) { + if (bftw_cache_shrink(cache, base) == 0) { + fd = openat(at_fd, at_path, flags); + } + } + + if (fd >= 0) { + if (cache->capacity == 0) { + bftw_cache_pop(cache); + } + + file->fd = fd; + bftw_cache_add(cache, file); + } + + return fd; +} + +/** + * Open a bftw_file. + * + * @param cache + * The cache to hold the file. + * @param file + * The file to open. + * @param path + * The full path to the file. + * @return + * The opened file descriptor, or negative on error. + */ +static int bftw_file_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) { + // Find the nearest open ancestor + struct bftw_file *base = file; + do { + base = base->parent; + } while (base && base->fd < 0); + + const char *at_path = path; + if (base) { + at_path += bftw_child_nameoff(base); + } + + int fd = bftw_file_openat(cache, file, base, at_path); + if (fd >= 0 || errno != ENAMETOOLONG) { + return fd; + } + + // Handle ENAMETOOLONG by manually traversing the path component-by-component + + // Use the ->next linked list to temporarily hold the reversed parent + // chain between base and file + struct bftw_file *cur; + for (cur = file; cur->parent != base; cur = cur->parent) { + cur->parent->next = cur; + } + + // Open the files in the chain one by one + for (base = cur; base; base = base->next) { + fd = bftw_file_openat(cache, base, base->parent, base->name); + if (fd < 0 || base == file) { + break; + } + } + + // Clear out the linked list + for (struct bftw_file *next = cur->next; cur != file; cur = next, next = next->next) { + cur->next = NULL; + } + + return fd; +} + +/** + * Open a bftw_file as a directory. + * + * @param cache + * The cache to hold the file. + * @param file + * The directory to open. + * @param path + * The full path to the directory. + * @return + * The opened directory, or NULL on error. + */ +static struct bfs_dir *bftw_file_opendir(struct bftw_cache *cache, struct bftw_file *file, const char *path) { + int fd = bftw_file_open(cache, file, path); + if (fd < 0) { + return NULL; + } + + return bfs_opendir(fd, NULL); +} + +/** Free a bftw_file. */ +static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) { + assert(file->refcount == 0); + + if (file->fd >= 0) { + bftw_file_close(cache, file); + } + + free(file); +} + +/** + * A queue of bftw_file's to examine. + */ +struct bftw_queue { + /** The head of the queue. */ + struct bftw_file *head; + /** The insertion target. */ + struct bftw_file **target; +}; + +/** Initialize a bftw_queue. */ +static void bftw_queue_init(struct bftw_queue *queue) { + queue->head = NULL; + queue->target = &queue->head; +} + +/** Add a file to a bftw_queue. */ +static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) { + assert(file->next == NULL); + + file->next = *queue->target; + *queue->target = file; + queue->target = &file->next; +} + +/** Pop the next file from the head of the queue. */ +static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) { + struct bftw_file *file = queue->head; + queue->head = file->next; + file->next = NULL; + if (queue->target == &file->next) { + queue->target = &queue->head; + } + return file; +} + +/** The split phase of mergesort. */ +static struct bftw_file **bftw_sort_split(struct bftw_file **head, struct bftw_file **tail) { + struct bftw_file **tortoise = head, **hare = head; + + while (*hare != *tail) { + tortoise = &(*tortoise)->next; + hare = &(*hare)->next; + if (*hare != *tail) { + hare = &(*hare)->next; + } + } + + return tortoise; +} + +/** The merge phase of mergesort. */ +static struct bftw_file **bftw_sort_merge(struct bftw_file **head, struct bftw_file **mid, struct bftw_file **tail) { + struct bftw_file *left = *head, *right = *mid, *end = *tail; + *mid = NULL; + *tail = NULL; + + while (left || right) { + struct bftw_file *next; + if (left && (!right || strcoll(left->name, right->name) <= 0)) { + next = left; + left = left->next; + } else { + next = right; + right = right->next; + } + + *head = next; + head = &next->next; + } + + *head = end; + return head; +} + +/** + * Sort a (sub-)list of files. + * + * @param head + * The head of the (sub-)list to sort. + * @param tail + * The tail of the (sub-)list to sort. + * @return + * The new tail of the (sub-)list. + */ +static struct bftw_file **bftw_sort_files(struct bftw_file **head, struct bftw_file **tail) { + struct bftw_file **mid = bftw_sort_split(head, tail); + if (*mid == *head || *mid == *tail) { + return tail; + } + + mid = bftw_sort_files(head, mid); + tail = bftw_sort_files(mid, tail); + + return bftw_sort_merge(head, mid, tail); +} + +/** + * Holds the current state of the bftw() traversal. + */ +struct bftw_state { + /** bftw() callback. */ + bftw_callback *callback; + /** bftw() callback data. */ + void *ptr; + /** bftw() flags. */ + enum bftw_flags flags; + /** Search strategy. */ + enum bftw_strategy strategy; + /** The mount table. */ + const struct bfs_mtab *mtab; + + /** The appropriate errno value, if any. */ + int error; + + /** The cache of open directories. */ + struct bftw_cache cache; + /** The queue of directories left to explore. */ + struct bftw_queue queue; + /** The start of the current batch of files. */ + struct bftw_file **batch; + + /** The current path. */ + char *path; + /** The current file. */ + struct bftw_file *file; + /** The previous file. */ + struct bftw_file *previous; + + /** The currently open directory. */ + struct bfs_dir *dir; + /** The current directory entry. */ + struct bfs_dirent *de; + /** Storage for the directory entry. */ + struct bfs_dirent de_storage; + /** Any error encountered while reading the directory. */ + int direrror; + + /** Extra data about the current file. */ + struct BFTW ftwbuf; +}; + +/** + * Initialize the bftw() state. + */ +static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) { + state->callback = args->callback; + state->ptr = args->ptr; + state->flags = args->flags; + state->strategy = args->strategy; + state->mtab = args->mtab; + + state->error = 0; + + if (args->nopenfd < 1) { + errno = EMFILE; + return -1; + } + + state->path = dstralloc(0); + if (!state->path) { + return -1; + } + + bftw_cache_init(&state->cache, args->nopenfd); + bftw_queue_init(&state->queue); + state->batch = NULL; + + state->file = NULL; + state->previous = NULL; + + state->dir = NULL; + state->de = NULL; + state->direrror = 0; + + return 0; +} + +/** Cached bfs_stat(). */ +static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, struct bftw_stat *cache, enum bfs_stat_flags flags) { + if (!cache->buf) { + if (cache->error) { + errno = cache->error; + } else if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, &cache->storage) == 0) { + cache->buf = &cache->storage; + } else { + cache->error = errno; + } + } + + return cache->buf; +} + +const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + struct BFTW *mutbuf = (struct BFTW *)ftwbuf; + const struct bfs_stat *ret; + + if (flags & BFS_STAT_NOFOLLOW) { + ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW); + if (ret && !S_ISLNK(ret->mode) && !mutbuf->stat_cache.buf) { + // Non-link, so share stat info + mutbuf->stat_cache.buf = ret; + } + } else { + ret = bftw_stat_impl(mutbuf, &mutbuf->stat_cache, BFS_STAT_FOLLOW); + if (!ret && (flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(errno)) { + ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW); + } + } + + return ret; +} + +const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + if (flags & BFS_STAT_NOFOLLOW) { + return ftwbuf->lstat_cache.buf; + } else if (ftwbuf->stat_cache.buf) { + return ftwbuf->stat_cache.buf; + } else if ((flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(ftwbuf->stat_cache.error)) { + return ftwbuf->lstat_cache.buf; + } else { + return NULL; + } +} + +enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + if (flags & BFS_STAT_NOFOLLOW) { + if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { + return ftwbuf->type; + } + } else if (flags & BFS_STAT_TRYFOLLOW) { + if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) { + return ftwbuf->type; + } + } else { + if (ftwbuf->type != BFS_LNK) { + return ftwbuf->type; + } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) { + return BFS_ERROR; + } + } + + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags); + if (statbuf) { + return bfs_mode_to_type(statbuf->mode); + } else { + return BFS_ERROR; + } +} + +/** + * Update the path for the current file. + */ +static int bftw_update_path(struct bftw_state *state, const char *name) { + const struct bftw_file *file = state->file; + size_t length = file ? file->nameoff + file->namelen : 0; + + assert(dstrlen(state->path) >= length); + dstresize(&state->path, length); + + if (name) { + if (length > 0 && state->path[length - 1] != '/') { + if (dstrapp(&state->path, '/') != 0) { + return -1; + } + } + if (dstrcat(&state->path, name) != 0) { + return -1; + } + } + + return 0; +} + +/** Check if a stat() call is needed for this visit. */ +static bool bftw_need_stat(const struct bftw_state *state) { + if (state->flags & BFTW_STAT) { + return true; + } + + const struct BFTW *ftwbuf = &state->ftwbuf; + if (ftwbuf->type == BFS_UNKNOWN) { + return true; + } + + if (ftwbuf->type == BFS_LNK && !(ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { + return true; + } + + if (ftwbuf->type == BFS_DIR) { + if (state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) { + return true; + } +#if __linux__ + } else if (state->mtab) { + // Linux fills in d_type from the underlying inode, even when + // the directory entry is a bind mount point. In that case, we + // need to stat() to get the correct type. We don't need to + // check for directories because they can only be mounted over + // by other directories. + if (bfs_might_be_mount(state->mtab, ftwbuf->path)) { + return true; + } +#endif + } + + return false; +} + +/** Initialize bftw_stat cache. */ +static void bftw_stat_init(struct bftw_stat *cache) { + cache->buf = NULL; + cache->error = 0; +} + +/** + * Open a file if necessary. + * + * @param file + * The file to open. + * @param path + * The path to that file or one of its descendants. + * @return + * The opened file descriptor, or -1 on error. + */ +static int bftw_ensure_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) { + int ret = file->fd; + + if (ret < 0) { + char *copy = strndup(path, file->nameoff + file->namelen); + if (!copy) { + return -1; + } + + ret = bftw_file_open(cache, file, copy); + free(copy); + } + + return ret; +} + +/** + * Initialize the buffers with data about the current path. + */ +static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { + struct bftw_file *file = state->file; + const struct bfs_dirent *de = state->de; + + struct BFTW *ftwbuf = &state->ftwbuf; + ftwbuf->path = state->path; + ftwbuf->root = file ? file->root->name : ftwbuf->path; + ftwbuf->depth = 0; + ftwbuf->visit = visit; + ftwbuf->type = BFS_UNKNOWN; + ftwbuf->error = state->direrror; + ftwbuf->at_fd = AT_FDCWD; + ftwbuf->at_path = ftwbuf->path; + ftwbuf->stat_flags = BFS_STAT_NOFOLLOW; + bftw_stat_init(&ftwbuf->lstat_cache); + bftw_stat_init(&ftwbuf->stat_cache); + + struct bftw_file *parent = NULL; + if (de) { + parent = file; + ftwbuf->depth = file->depth + 1; + ftwbuf->type = de->type; + ftwbuf->nameoff = bftw_child_nameoff(file); + } else if (file) { + parent = file->parent; + ftwbuf->depth = file->depth; + ftwbuf->type = file->type; + ftwbuf->nameoff = file->nameoff; + } + + if (parent) { + // Try to ensure the immediate parent is open, to avoid ENAMETOOLONG + if (bftw_ensure_open(&state->cache, parent, state->path) >= 0) { + ftwbuf->at_fd = parent->fd; + ftwbuf->at_path += ftwbuf->nameoff; + } else { + ftwbuf->error = errno; + } + } + + if (ftwbuf->depth == 0) { + // Compute the name offset for root paths like "foo/bar" + ftwbuf->nameoff = xbasename(ftwbuf->path) - ftwbuf->path; + } + + if (ftwbuf->error != 0) { + ftwbuf->type = BFS_ERROR; + return; + } + + int follow_flags = BFTW_FOLLOW_ALL; + if (ftwbuf->depth == 0) { + follow_flags |= BFTW_FOLLOW_ROOTS; + } + bool follow = state->flags & follow_flags; + if (follow) { + ftwbuf->stat_flags = BFS_STAT_TRYFOLLOW; + } + + const struct bfs_stat *statbuf = NULL; + if (bftw_need_stat(state)) { + statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (statbuf) { + ftwbuf->type = bfs_mode_to_type(statbuf->mode); + } else { + ftwbuf->type = BFS_ERROR; + ftwbuf->error = errno; + return; + } + } + + if (ftwbuf->type == BFS_DIR && (state->flags & BFTW_DETECT_CYCLES)) { + for (const struct bftw_file *ancestor = parent; ancestor; ancestor = ancestor->parent) { + if (ancestor->dev == statbuf->dev && ancestor->ino == statbuf->ino) { + ftwbuf->type = BFS_ERROR; + ftwbuf->error = ELOOP; + return; + } + } + } +} + +/** Check if the current file is a mount point. */ +static bool bftw_is_mount(struct bftw_state *state, const char *name) { + const struct bftw_file *file = state->file; + if (!file) { + return false; + } + + const struct bftw_file *parent = name ? file : file->parent; + if (!parent) { + return false; + } + + const struct BFTW *ftwbuf = &state->ftwbuf; + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + return statbuf && statbuf->dev != parent->dev; +} + +/** Fill file identity information from an ftwbuf. */ +static void bftw_fill_id(struct bftw_file *file, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf; + if (!statbuf || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { + statbuf = ftwbuf->lstat_cache.buf; + } + if (statbuf) { + file->dev = statbuf->dev; + file->ino = statbuf->ino; + } +} + +/** + * Visit a path, invoking the callback. + */ +static enum bftw_action bftw_visit(struct bftw_state *state, const char *name, enum bftw_visit visit) { + if (bftw_update_path(state, name) != 0) { + state->error = errno; + return BFTW_STOP; + } + + const struct BFTW *ftwbuf = &state->ftwbuf; + bftw_init_ftwbuf(state, visit); + + // Never give the callback BFS_ERROR unless BFTW_RECOVER is specified + if (ftwbuf->type == BFS_ERROR && !(state->flags & BFTW_RECOVER)) { + state->error = ftwbuf->error; + return BFTW_STOP; + } + + if ((state->flags & BFTW_SKIP_MOUNTS) && bftw_is_mount(state, name)) { + return BFTW_PRUNE; + } + + enum bftw_action ret = state->callback(ftwbuf, state->ptr); + switch (ret) { + case BFTW_CONTINUE: + break; + case BFTW_PRUNE: + case BFTW_STOP: + goto done; + default: + state->error = EINVAL; + return BFTW_STOP; + } + + if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) { + ret = BFTW_PRUNE; + goto done; + } + + if ((state->flags & BFTW_PRUNE_MOUNTS) && bftw_is_mount(state, name)) { + ret = BFTW_PRUNE; + goto done; + } + +done: + if (state->file && !name) { + bftw_fill_id(state->file, ftwbuf); + } + + return ret; +} + +/** + * Push a new file onto the queue. + */ +static int bftw_push(struct bftw_state *state, const char *name, bool fill_id) { + struct bftw_file *parent = state->file; + struct bftw_file *file = bftw_file_new(parent, name); + if (!file) { + state->error = errno; + return -1; + } + + if (state->de) { + file->type = state->de->type; + } + + if (fill_id) { + bftw_fill_id(file, &state->ftwbuf); + } + + bftw_queue_push(&state->queue, file); + + return 0; +} + +/** + * Build the path to the current file. + */ +static int bftw_build_path(struct bftw_state *state) { + const struct bftw_file *file = state->file; + + size_t pathlen = file->nameoff + file->namelen; + if (dstresize(&state->path, pathlen) != 0) { + state->error = errno; + return -1; + } + + // Try to find a common ancestor with the existing path + const struct bftw_file *ancestor = state->previous; + while (ancestor && ancestor->depth > file->depth) { + ancestor = ancestor->parent; + } + + // Build the path backwards + while (file && file != ancestor) { + if (file->nameoff > 0) { + state->path[file->nameoff - 1] = '/'; + } + memcpy(state->path + file->nameoff, file->name, file->namelen); + + if (ancestor && ancestor->depth == file->depth) { + ancestor = ancestor->parent; + } + file = file->parent; + } + + state->previous = state->file; + return 0; +} + +/** + * Pop the next file from the queue. + */ +static int bftw_pop(struct bftw_state *state) { + if (!state->queue.head) { + return 0; + } + + state->file = bftw_queue_pop(&state->queue); + + if (bftw_build_path(state) != 0) { + return -1; + } + + return 1; +} + +/** + * Open the current directory. + */ +static void bftw_opendir(struct bftw_state *state) { + assert(!state->dir); + assert(!state->de); + + state->direrror = 0; + + state->dir = bftw_file_opendir(&state->cache, state->file, state->path); + if (!state->dir) { + state->direrror = errno; + } +} + +/** + * Read an entry from the current directory. + */ +static int bftw_readdir(struct bftw_state *state) { + if (!state->dir) { + return -1; + } + + int ret = bfs_readdir(state->dir, &state->de_storage); + if (ret > 0) { + state->de = &state->de_storage; + } else if (ret == 0) { + state->de = NULL; + } else { + state->de = NULL; + state->direrror = errno; + } + + return ret; +} + +/** + * Flags controlling which files get visited when done with a directory. + */ +enum bftw_gc_flags { + /** Don't visit anything. */ + BFTW_VISIT_NONE = 0, + /** Visit the file itself. */ + BFTW_VISIT_FILE = 1 << 0, + /** Visit the file's ancestors. */ + BFTW_VISIT_PARENTS = 1 << 1, + /** Visit both the file and its ancestors. */ + BFTW_VISIT_ALL = BFTW_VISIT_FILE | BFTW_VISIT_PARENTS, +}; + +/** + * Close the current directory. + */ +static enum bftw_action bftw_closedir(struct bftw_state *state, enum bftw_gc_flags flags) { + struct bftw_file *file = state->file; + enum bftw_action ret = BFTW_CONTINUE; + + if (state->dir) { + assert(file->fd >= 0); + + if (file->refcount > 1) { + // Keep the fd around if any subdirectories exist + file->fd = bfs_freedir(state->dir); + } else { + bfs_closedir(state->dir); + file->fd = -1; + } + + if (file->fd < 0) { + bftw_cache_remove(&state->cache, file); + } + } + + state->de = NULL; + state->dir = NULL; + + if (state->direrror != 0) { + if (flags & BFTW_VISIT_FILE) { + ret = bftw_visit(state, NULL, BFTW_PRE); + } else { + state->error = state->direrror; + } + state->direrror = 0; + } + + return ret; +} + +/** + * Finalize and free a file we're done with. + */ +static enum bftw_action bftw_gc_file(struct bftw_state *state, enum bftw_gc_flags flags) { + enum bftw_action ret = BFTW_CONTINUE; + + if (!(state->flags & BFTW_POST_ORDER)) { + flags = 0; + } + bool visit = flags & BFTW_VISIT_FILE; + + while (state->file) { + struct bftw_file *file = state->file; + if (--file->refcount > 0) { + state->file = NULL; + break; + } + + if (visit && bftw_visit(state, NULL, BFTW_POST) == BFTW_STOP) { + ret = BFTW_STOP; + flags &= ~BFTW_VISIT_PARENTS; + } + visit = flags & BFTW_VISIT_PARENTS; + + struct bftw_file *parent = file->parent; + if (state->previous == file) { + state->previous = parent; + } + bftw_file_free(&state->cache, file); + state->file = parent; + } + + return ret; +} + +/** + * Drain all the entries from a bftw_queue. + */ +static void bftw_drain_queue(struct bftw_state *state, struct bftw_queue *queue) { + while (queue->head) { + state->file = bftw_queue_pop(queue); + bftw_gc_file(state, BFTW_VISIT_NONE); + } +} + +/** + * Dispose of the bftw() state. + * + * @return + * The bftw() return value. + */ +static int bftw_state_destroy(struct bftw_state *state) { + dstrfree(state->path); + + bftw_closedir(state, BFTW_VISIT_NONE); + + bftw_gc_file(state, BFTW_VISIT_NONE); + bftw_drain_queue(state, &state->queue); + + bftw_cache_destroy(&state->cache); + + errno = state->error; + return state->error ? -1 : 0; +} + +/** Start a batch of files. */ +static void bftw_batch_start(struct bftw_state *state) { + if (state->strategy == BFTW_DFS) { + state->queue.target = &state->queue.head; + } + state->batch = state->queue.target; +} + +/** Finish adding a batch of files. */ +static void bftw_batch_finish(struct bftw_state *state) { + if (state->flags & BFTW_SORT) { + state->queue.target = bftw_sort_files(state->batch, state->queue.target); + } +} + +/** + * Streaming mode: visit files as they are encountered. + */ +static int bftw_stream(const struct bftw_args *args) { + struct bftw_state state; + if (bftw_state_init(&state, args) != 0) { + return -1; + } + + assert(!(state.flags & (BFTW_SORT | BFTW_BUFFER))); + + bftw_batch_start(&state); + for (size_t i = 0; i < args->npaths; ++i) { + const char *path = args->paths[i]; + + switch (bftw_visit(&state, path, BFTW_PRE)) { + case BFTW_CONTINUE: + break; + case BFTW_PRUNE: + continue; + case BFTW_STOP: + goto done; + } + + if (bftw_push(&state, path, true) != 0) { + goto done; + } + } + bftw_batch_finish(&state); + + while (bftw_pop(&state) > 0) { + bftw_opendir(&state); + + bftw_batch_start(&state); + while (bftw_readdir(&state) > 0) { + const char *name = state.de->name; + + switch (bftw_visit(&state, name, BFTW_PRE)) { + case BFTW_CONTINUE: + break; + case BFTW_PRUNE: + continue; + case BFTW_STOP: + goto done; + } + + if (bftw_push(&state, name, true) != 0) { + goto done; + } + } + bftw_batch_finish(&state); + + if (bftw_closedir(&state, BFTW_VISIT_ALL) == BFTW_STOP) { + goto done; + } + if (bftw_gc_file(&state, BFTW_VISIT_ALL) == BFTW_STOP) { + goto done; + } + } + +done: + return bftw_state_destroy(&state); +} + +/** + * Batching mode: queue up all children before visiting them. + */ +static int bftw_batch(const struct bftw_args *args) { + struct bftw_state state; + if (bftw_state_init(&state, args) != 0) { + return -1; + } + + bftw_batch_start(&state); + for (size_t i = 0; i < args->npaths; ++i) { + if (bftw_push(&state, args->paths[i], false) != 0) { + goto done; + } + } + bftw_batch_finish(&state); + + while (bftw_pop(&state) > 0) { + enum bftw_gc_flags gcflags = BFTW_VISIT_ALL; + + switch (bftw_visit(&state, NULL, BFTW_PRE)) { + case BFTW_CONTINUE: + break; + case BFTW_PRUNE: + gcflags &= ~BFTW_VISIT_FILE; + goto next; + case BFTW_STOP: + goto done; + } + + bftw_opendir(&state); + + bftw_batch_start(&state); + while (bftw_readdir(&state) > 0) { + if (bftw_push(&state, state.de->name, false) != 0) { + goto done; + } + } + bftw_batch_finish(&state); + + if (bftw_closedir(&state, gcflags) == BFTW_STOP) { + goto done; + } + + next: + if (bftw_gc_file(&state, gcflags) == BFTW_STOP) { + goto done; + } + } + +done: + return bftw_state_destroy(&state); +} + +/** Select bftw_stream() or bftw_batch() appropriately. */ +static int bftw_auto(const struct bftw_args *args) { + if (args->flags & (BFTW_SORT | BFTW_BUFFER)) { + return bftw_batch(args); + } else { + return bftw_stream(args); + } +} + +/** + * Iterative deepening search state. + */ +struct bftw_ids_state { + /** The wrapped callback. */ + bftw_callback *delegate; + /** The wrapped callback arguments. */ + void *ptr; + /** Which visit this search corresponds to. */ + enum bftw_visit visit; + /** Whether to override the bftw_visit. */ + bool force_visit; + /** The current minimum depth (inclusive). */ + size_t min_depth; + /** The current maximum depth (exclusive). */ + size_t max_depth; + /** The set of pruned paths. */ + struct trie pruned; + /** An error code to report. */ + int error; + /** Whether the bottom has been found. */ + bool bottom; + /** Whether to quit the search. */ + bool quit; +}; + +/** Iterative deepening callback function. */ +static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr) { + struct bftw_ids_state *state = ptr; + + if (state->force_visit) { + struct BFTW *mutbuf = (struct BFTW *)ftwbuf; + mutbuf->visit = state->visit; + } + + if (ftwbuf->type == BFS_ERROR) { + if (ftwbuf->depth + 1 >= state->min_depth) { + return state->delegate(ftwbuf, state->ptr); + } else { + return BFTW_PRUNE; + } + } + + if (ftwbuf->depth < state->min_depth) { + if (trie_find_str(&state->pruned, ftwbuf->path)) { + return BFTW_PRUNE; + } else { + return BFTW_CONTINUE; + } + } else if (state->visit == BFTW_POST) { + if (trie_find_str(&state->pruned, ftwbuf->path)) { + return BFTW_PRUNE; + } + } + + enum bftw_action ret = BFTW_CONTINUE; + if (ftwbuf->visit == state->visit) { + ret = state->delegate(ftwbuf, state->ptr); + } + + switch (ret) { + case BFTW_CONTINUE: + if (ftwbuf->type == BFS_DIR && ftwbuf->depth + 1 >= state->max_depth) { + state->bottom = false; + ret = BFTW_PRUNE; + } + break; + case BFTW_PRUNE: + if (ftwbuf->type == BFS_DIR) { + if (!trie_insert_str(&state->pruned, ftwbuf->path)) { + state->error = errno; + state->quit = true; + ret = BFTW_STOP; + } + } + break; + case BFTW_STOP: + state->quit = true; + break; + } + + return ret; +} + +/** Initialize iterative deepening state. */ +static void bftw_ids_init(const struct bftw_args *args, struct bftw_ids_state *state, struct bftw_args *ids_args) { + state->delegate = args->callback; + state->ptr = args->ptr; + state->visit = BFTW_PRE; + state->force_visit = false; + state->min_depth = 0; + state->max_depth = 1; + trie_init(&state->pruned); + state->error = 0; + state->bottom = false; + state->quit = false; + + *ids_args = *args; + ids_args->callback = bftw_ids_callback; + ids_args->ptr = state; + ids_args->flags &= ~BFTW_POST_ORDER; + ids_args->strategy = BFTW_DFS; +} + +/** Finish an iterative deepening search. */ +static int bftw_ids_finish(struct bftw_ids_state *state) { + int ret = 0; + + if (state->error) { + ret = -1; + } else { + state->error = errno; + } + + trie_destroy(&state->pruned); + + errno = state->error; + return ret; +} + +/** + * Iterative deepening bftw() wrapper. + */ +static int bftw_ids(const struct bftw_args *args) { + struct bftw_ids_state state; + struct bftw_args ids_args; + bftw_ids_init(args, &state, &ids_args); + + while (!state.quit && !state.bottom) { + state.bottom = true; + + if (bftw_auto(&ids_args) != 0) { + state.error = errno; + state.quit = true; + } + + ++state.min_depth; + ++state.max_depth; + } + + if (args->flags & BFTW_POST_ORDER) { + state.visit = BFTW_POST; + state.force_visit = true; + + while (!state.quit && state.min_depth > 0) { + --state.max_depth; + --state.min_depth; + + if (bftw_auto(&ids_args) != 0) { + state.error = errno; + state.quit = true; + } + } + } + + return bftw_ids_finish(&state); +} + +/** + * Exponential deepening bftw() wrapper. + */ +static int bftw_eds(const struct bftw_args *args) { + struct bftw_ids_state state; + struct bftw_args ids_args; + bftw_ids_init(args, &state, &ids_args); + + while (!state.quit && !state.bottom) { + state.bottom = true; + + if (bftw_auto(&ids_args) != 0) { + state.error = errno; + state.quit = true; + } + + state.min_depth = state.max_depth; + state.max_depth *= 2; + } + + if (!state.quit && (args->flags & BFTW_POST_ORDER)) { + state.visit = BFTW_POST; + state.min_depth = 0; + ids_args.flags |= BFTW_POST_ORDER; + + if (bftw_auto(&ids_args) != 0) { + state.error = errno; + } + } + + return bftw_ids_finish(&state); +} + +int bftw(const struct bftw_args *args) { + switch (args->strategy) { + case BFTW_BFS: + return bftw_auto(args); + case BFTW_DFS: + return bftw_batch(args); + case BFTW_IDS: + return bftw_ids(args); + case BFTW_EDS: + return bftw_eds(args); + } + + errno = EINVAL; + return -1; +} diff --git a/src/bftw.h b/src/bftw.h new file mode 100644 index 0000000..c458e1b --- /dev/null +++ b/src/bftw.h @@ -0,0 +1,223 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2015-2021 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. * + ****************************************************************************/ + +/** + * A file-walking API based on nftw(). + */ + +#ifndef BFS_BFTW_H +#define BFS_BFTW_H + +#include "dir.h" +#include "stat.h" +#include <stddef.h> + +/** + * Possible visit occurrences. + */ +enum bftw_visit { + /** Pre-order visit. */ + BFTW_PRE, + /** Post-order visit. */ + BFTW_POST, +}; + +/** + * Cached bfs_stat() info for a file. + */ +struct bftw_stat { + /** A pointer to the bfs_stat() buffer, if available. */ + const struct bfs_stat *buf; + /** Storage for the bfs_stat() buffer, if needed. */ + struct bfs_stat storage; + /** The cached error code, if any. */ + int error; +}; + +/** + * Data about the current file for the bftw() callback. + */ +struct BFTW { + /** The path to the file. */ + const char *path; + /** The string offset of the filename. */ + size_t nameoff; + + /** The root path passed to bftw(). */ + const char *root; + /** The depth of this file in the traversal. */ + size_t depth; + /** Which visit this is. */ + enum bftw_visit visit; + + /** The file type. */ + enum bfs_type type; + /** The errno that occurred, if type == BFTW_ERROR. */ + int error; + + /** A parent file descriptor for the *at() family of calls. */ + int at_fd; + /** The path relative to at_fd for the *at() family of calls. */ + const char *at_path; + + /** Flags for bfs_stat(). */ + enum bfs_stat_flags stat_flags; + /** Cached bfs_stat() info for BFS_STAT_NOFOLLOW. */ + struct bftw_stat lstat_cache; + /** Cached bfs_stat() info for BFS_STAT_FOLLOW. */ + struct bftw_stat stat_cache; +}; + +/** + * Get bfs_stat() info for a file encountered during bftw(), caching the result + * whenever possible. + * + * @param ftwbuf + * bftw() data for the file to stat. + * @param flags + * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. + * @return + * A pointer to a bfs_stat() buffer, or NULL if the call failed. + */ +const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags); + +/** + * Get bfs_stat() info for a file encountered during bftw(), if it has already + * been cached. + * + * @param ftwbuf + * bftw() data for the file to stat. + * @param flags + * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. + * @return + * A pointer to a bfs_stat() buffer, or NULL if no stat info is cached. + */ +const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags); + +/** + * Get the type of a file encountered during bftw(), with flags controlling + * whether to follow links. This function will avoid calling bfs_stat() if + * possible. + * + * @param ftwbuf + * bftw() data for the file to check. + * @param flags + * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. + * @return + * The type of the file, or BFTW_ERROR if an error occurred. + */ +enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags); + +/** + * Walk actions returned by the bftw() callback. + */ +enum bftw_action { + /** Keep walking. */ + BFTW_CONTINUE, + /** Skip this path's children. */ + BFTW_PRUNE, + /** Stop walking. */ + BFTW_STOP, +}; + +/** + * Callback function type for bftw(). + * + * @param ftwbuf + * Data about the current file. + * @param ptr + * The pointer passed to bftw(). + * @return + * An action value. + */ +typedef enum bftw_action bftw_callback(const struct BFTW *ftwbuf, void *ptr); + +/** + * Flags that control bftw() behavior. + */ +enum bftw_flags { + /** stat() each encountered file. */ + BFTW_STAT = 1 << 0, + /** Attempt to recover from encountered errors. */ + BFTW_RECOVER = 1 << 1, + /** Visit directories in post-order as well as pre-order. */ + BFTW_POST_ORDER = 1 << 2, + /** If the initial path is a symbolic link, follow it. */ + BFTW_FOLLOW_ROOTS = 1 << 3, + /** Follow all symbolic links. */ + BFTW_FOLLOW_ALL = 1 << 4, + /** Detect directory cycles. */ + BFTW_DETECT_CYCLES = 1 << 5, + /** Skip mount points and their descendents. */ + BFTW_SKIP_MOUNTS = 1 << 6, + /** Skip the descendents of mount points. */ + BFTW_PRUNE_MOUNTS = 1 << 7, + /** Sort directory entries before processing them. */ + BFTW_SORT = 1 << 8, + /** Read each directory into memory before processing its children. */ + BFTW_BUFFER = 1 << 9, +}; + +/** + * Tree search strategies for bftw(). + */ +enum bftw_strategy { + /** Breadth-first search. */ + BFTW_BFS, + /** Depth-first search. */ + BFTW_DFS, + /** Iterative deepening search. */ + BFTW_IDS, + /** Exponential deepening search. */ + BFTW_EDS, +}; + +/** + * Structure for holding the arguments passed to bftw(). + */ +struct bftw_args { + /** The path(s) to start from. */ + const char **paths; + /** The number of starting paths. */ + size_t npaths; + /** The callback to invoke. */ + bftw_callback *callback; + /** A pointer which is passed to the callback. */ + void *ptr; + /** The maximum number of file descriptors to keep open. */ + int nopenfd; + /** Flags that control bftw() behaviour. */ + enum bftw_flags flags; + /** The search strategy to use. */ + enum bftw_strategy strategy; + /** The parsed mount table, if available. */ + const struct bfs_mtab *mtab; +}; + +/** + * Breadth First Tree Walk (or Better File Tree Walk). + * + * Like ftw(3) and nftw(3), this function walks a directory tree recursively, + * and invokes a callback for each path it encounters. + * + * @param args + * The arguments that control the walk. + * @return + * 0 on success, or -1 on failure. + */ +int bftw(const struct bftw_args *args); + +#endif // BFS_BFTW_H diff --git a/src/color.c b/src/color.c new file mode 100644 index 0000000..9e267da --- /dev/null +++ b/src/color.c @@ -0,0 +1,1125 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +#include "color.h" +#include "bftw.h" +#include "dir.h" +#include "dstring.h" +#include "expr.h" +#include "fsade.h" +#include "stat.h" +#include "trie.h" +#include "util.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +struct colors { + char *reset; + char *leftcode; + char *rightcode; + char *endcode; + char *clear_to_eol; + + char *bold; + char *gray; + char *red; + char *green; + char *yellow; + char *blue; + char *magenta; + char *cyan; + char *white; + + char *warning; + char *error; + + char *normal; + + char *file; + char *multi_hard; + char *executable; + char *capable; + char *setgid; + char *setuid; + + char *directory; + char *sticky; + char *other_writable; + char *sticky_other_writable; + + char *link; + char *orphan; + char *missing; + bool link_as_target; + + char *blockdev; + char *chardev; + char *door; + char *pipe; + char *socket; + + /** A mapping from color names (fi, di, ln, etc.) to struct fields. */ + struct trie names; + + /** A mapping from file extensions to colors. */ + struct trie ext_colors; +}; + +/** Initialize a color in the table. */ +static int init_color(struct colors *colors, const char *name, const char *value, char **field) { + if (value) { + *field = dstrdup(value); + if (!*field) { + return -1; + } + } else { + *field = NULL; + } + + struct trie_leaf *leaf = trie_insert_str(&colors->names, name); + if (leaf) { + leaf->value = field; + return 0; + } else { + return -1; + } +} + +/** Get a color from the table. */ +static char **get_color(const struct colors *colors, const char *name) { + const struct trie_leaf *leaf = trie_find_str(&colors->names, name); + if (leaf) { + return (char **)leaf->value; + } else { + return NULL; + } +} + +/** Set the value of a color. */ +static int set_color(struct colors *colors, const char *name, char *value) { + char **color = get_color(colors, name); + if (color) { + dstrfree(*color); + *color = value; + return 0; + } else { + return -1; + } +} + +/** + * Transform a file extension for fast lookups, by reversing and lowercasing it. + */ +static void extxfrm(char *ext) { + size_t len = strlen(ext); + for (size_t i = 0; i < len - i; ++i) { + char a = ext[i]; + char b = ext[len - i - 1]; + + // What's internationalization? Doesn't matter, this is what + // GNU ls does. Luckily, since there's no standard C way to + // casefold. Not using tolower() here since it respects the + // current locale, which GNU ls doesn't do. + if (a >= 'A' && a <= 'Z') { + a += 'a' - 'A'; + } + if (b >= 'A' && b <= 'Z') { + b += 'a' - 'A'; + } + + ext[i] = b; + ext[len - i - 1] = a; + } +} + +/** + * Set the color for an extension. + */ +static int set_ext_color(struct colors *colors, char *key, const char *value) { + extxfrm(key); + + // A later *.x should override any earlier *.x, *.y.x, etc. + struct trie_leaf *match; + while ((match = trie_find_postfix(&colors->ext_colors, key))) { + dstrfree(match->value); + trie_remove(&colors->ext_colors, match); + } + + struct trie_leaf *leaf = trie_insert_str(&colors->ext_colors, key); + if (leaf) { + leaf->value = (char *)value; + return 0; + } else { + return -1; + } +} + +/** + * Find a color by an extension. + */ +static const char *get_ext_color(const struct colors *colors, const char *filename) { + char *xfrm = strdup(filename); + if (!xfrm) { + return NULL; + } + extxfrm(xfrm); + + const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_colors, xfrm); + free(xfrm); + if (leaf) { + return leaf->value; + } else { + return NULL; + } +} + +/** + * Parse a chunk of $LS_COLORS that may have escape sequences. The supported + * escapes are: + * + * \a, \b, \f, \n, \r, \t, \v: + * As in C + * \e: + * ESC (\033) + * \?: + * DEL (\177) + * \_: + * ' ' (space) + * \NNN: + * Octal + * \xNN: + * Hex + * ^C: + * Control character. + * + * See man dir_colors. + * + * @param value + * The value to parse. + * @param end + * The character that marks the end of the chunk. + * @param[out] next + * Will be set to the next chunk. + * @return + * The parsed chunk as a dstring. + */ +static char *unescape(const char *value, char end, const char **next) { + if (!value) { + goto fail; + } + + char *str = dstralloc(0); + if (!str) { + goto fail_str; + } + + const char *i; + for (i = value; *i && *i != end; ++i) { + unsigned char c = 0; + + switch (*i) { + case '\\': + switch (*++i) { + case 'a': + c = '\a'; + break; + case 'b': + c = '\b'; + break; + case 'e': + c = '\033'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'v': + c = '\v'; + break; + case '?': + c = '\177'; + break; + case '_': + c = ' '; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + while (i[1] >= '0' && i[1] <= '7') { + c <<= 3; + c |= *i++ - '0'; + } + c <<= 3; + c |= *i - '0'; + break; + + case 'X': + case 'x': + while (true) { + if (i[1] >= '0' && i[1] <= '9') { + c <<= 4; + c |= i[1] - '0'; + } else if (i[1] >= 'A' && i[1] <= 'F') { + c <<= 4; + c |= i[1] - 'A' + 0xA; + } else if (i[1] >= 'a' && i[1] <= 'f') { + c <<= 4; + c |= i[1] - 'a' + 0xA; + } else { + break; + } + ++i; + } + break; + + case '\0': + goto fail_str; + + default: + c = *i; + break; + } + break; + + case '^': + switch (*++i) { + case '?': + c = '\177'; + break; + case '\0': + goto fail_str; + default: + // CTRL masks bits 6 and 7 + c = *i & 0x1F; + break; + } + break; + + default: + c = *i; + break; + } + + if (dstrapp(&str, c) != 0) { + goto fail_str; + } + } + + if (*i) { + *next = i + 1; + } else { + *next = NULL; + } + + return str; + +fail_str: + dstrfree(str); +fail: + *next = NULL; + return NULL; +} + +/** Parse the GNU $LS_COLORS format. */ +static void parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { + for (const char *chunk = ls_colors, *next; chunk; chunk = next) { + if (chunk[0] == '*') { + char *key = unescape(chunk + 1, '=', &next); + if (!key) { + continue; + } + + char *value = unescape(next, ':', &next); + if (value) { + if (set_ext_color(colors, key, value) != 0) { + dstrfree(value); + } + } + + dstrfree(key); + } else { + const char *equals = strchr(chunk, '='); + if (!equals) { + break; + } + + char *value = unescape(equals + 1, ':', &next); + if (!value) { + continue; + } + + char *key = strndup(chunk, equals - chunk); + if (!key) { + dstrfree(value); + continue; + } + + // All-zero values should be treated like NULL, to fall + // back on any other relevant coloring for that file + if (strspn(value, "0") == strlen(value) + && strcmp(key, "rs") != 0 + && strcmp(key, "lc") != 0 + && strcmp(key, "rc") != 0 + && strcmp(key, "ec") != 0) { + dstrfree(value); + value = NULL; + } + + if (set_color(colors, key, value) != 0) { + dstrfree(value); + } + free(key); + } + } + + if (colors->link && strcmp(colors->link, "target") == 0) { + colors->link_as_target = true; + dstrfree(colors->link); + colors->link = NULL; + } +} + +struct colors *parse_colors() { + struct colors *colors = malloc(sizeof(struct colors)); + if (!colors) { + return NULL; + } + + trie_init(&colors->names); + trie_init(&colors->ext_colors); + + int ret = 0; + + // From man console_codes + + ret |= init_color(colors, "rs", "0", &colors->reset); + ret |= init_color(colors, "lc", "\033[", &colors->leftcode); + ret |= init_color(colors, "rc", "m", &colors->rightcode); + ret |= init_color(colors, "ec", NULL, &colors->endcode); + ret |= init_color(colors, "cl", "\033[K", &colors->clear_to_eol); + + ret |= init_color(colors, "bld", "01;39", &colors->bold); + ret |= init_color(colors, "gry", "01;30", &colors->gray); + ret |= init_color(colors, "red", "01;31", &colors->red); + ret |= init_color(colors, "grn", "01;32", &colors->green); + ret |= init_color(colors, "ylw", "01;33", &colors->yellow); + ret |= init_color(colors, "blu", "01;34", &colors->blue); + ret |= init_color(colors, "mag", "01;35", &colors->magenta); + ret |= init_color(colors, "cyn", "01;36", &colors->cyan); + ret |= init_color(colors, "wht", "01;37", &colors->white); + + ret |= init_color(colors, "wrn", "01;33", &colors->warning); + ret |= init_color(colors, "err", "01;31", &colors->error); + + // Defaults from man dir_colors + + ret |= init_color(colors, "no", NULL, &colors->normal); + + ret |= init_color(colors, "fi", NULL, &colors->file); + ret |= init_color(colors, "mh", NULL, &colors->multi_hard); + ret |= init_color(colors, "ex", "01;32", &colors->executable); + ret |= init_color(colors, "ca", "30;41", &colors->capable); + ret |= init_color(colors, "sg", "30;43", &colors->setgid); + ret |= init_color(colors, "su", "37;41", &colors->setuid); + + ret |= init_color(colors, "di", "01;34", &colors->directory); + ret |= init_color(colors, "st", "37;44", &colors->sticky); + ret |= init_color(colors, "ow", "34;42", &colors->other_writable); + ret |= init_color(colors, "tw", "30;42", &colors->sticky_other_writable); + + ret |= init_color(colors, "ln", "01;36", &colors->link); + ret |= init_color(colors, "or", NULL, &colors->orphan); + ret |= init_color(colors, "mi", NULL, &colors->missing); + colors->link_as_target = false; + + ret |= init_color(colors, "bd", "01;33", &colors->blockdev); + ret |= init_color(colors, "cd", "01;33", &colors->chardev); + ret |= init_color(colors, "do", "01;35", &colors->door); + ret |= init_color(colors, "pi", "33", &colors->pipe); + ret |= init_color(colors, "so", "01;35", &colors->socket); + + if (ret) { + free_colors(colors); + return NULL; + } + + parse_gnu_ls_colors(colors, getenv("LS_COLORS")); + parse_gnu_ls_colors(colors, getenv("BFS_COLORS")); + + return colors; +} + +void free_colors(struct colors *colors) { + if (colors) { + struct trie_leaf *leaf; + while ((leaf = trie_first_leaf(&colors->ext_colors))) { + dstrfree(leaf->value); + trie_remove(&colors->ext_colors, leaf); + } + trie_destroy(&colors->ext_colors); + + while ((leaf = trie_first_leaf(&colors->names))) { + char **field = leaf->value; + dstrfree(*field); + trie_remove(&colors->names, leaf); + } + trie_destroy(&colors->names); + + free(colors); + } +} + +CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) { + CFILE *cfile = malloc(sizeof(*cfile)); + if (!cfile) { + return NULL; + } + + cfile->buffer = dstralloc(128); + if (!cfile->buffer) { + free(cfile); + return NULL; + } + + cfile->file = file; + cfile->close = close; + + if (isatty(fileno(file))) { + cfile->colors = colors; + } else { + cfile->colors = NULL; + } + + return cfile; +} + +int cfclose(CFILE *cfile) { + int ret = 0; + + if (cfile) { + dstrfree(cfile->buffer); + + if (cfile->close) { + ret = fclose(cfile->file); + } + + free(cfile); + } + + return ret; +} + +/** Check if a symlink is broken. */ +static bool is_link_broken(const struct BFTW *ftwbuf) { + if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) { + return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0; + } else { + return true; + } +} + +/** Get the color for a file. */ +static const char *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + enum bfs_type type = bftw_type(ftwbuf, flags); + if (type == BFS_ERROR) { + goto error; + } + + const struct bfs_stat *statbuf = NULL; + const char *color = NULL; + + switch (type) { + case BFS_REG: + if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) { + statbuf = bftw_stat(ftwbuf, flags); + if (!statbuf) { + goto error; + } + } + + if (colors->setuid && (statbuf->mode & 04000)) { + color = colors->setuid; + } else if (colors->setgid && (statbuf->mode & 02000)) { + color = colors->setgid; + } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) { + color = colors->capable; + } else if (colors->executable && (statbuf->mode & 00111)) { + color = colors->executable; + } else if (colors->multi_hard && statbuf->nlink > 1) { + color = colors->multi_hard; + } + + if (!color) { + color = get_ext_color(colors, filename); + } + + if (!color) { + color = colors->file; + } + + break; + + case BFS_DIR: + if (colors->sticky_other_writable || colors->other_writable || colors->sticky) { + statbuf = bftw_stat(ftwbuf, flags); + if (!statbuf) { + goto error; + } + } + + if (colors->sticky_other_writable && (statbuf->mode & 01002) == 01002) { + color = colors->sticky_other_writable; + } else if (colors->other_writable && (statbuf->mode & 00002)) { + color = colors->other_writable; + } else if (colors->sticky && (statbuf->mode & 01000)) { + color = colors->sticky; + } else { + color = colors->directory; + } + + break; + + case BFS_LNK: + if (colors->orphan && is_link_broken(ftwbuf)) { + color = colors->orphan; + } else { + color = colors->link; + } + break; + + case BFS_BLK: + color = colors->blockdev; + break; + case BFS_CHR: + color = colors->chardev; + break; + case BFS_FIFO: + color = colors->pipe; + break; + case BFS_SOCK: + color = colors->socket; + break; + case BFS_DOOR: + color = colors->door; + break; + + default: + break; + } + + if (!color) { + color = colors->normal; + } + + return color; + +error: + if (colors->missing) { + return colors->missing; + } else { + return colors->orphan; + } +} + +/** Print an ANSI escape sequence. */ +static int print_esc(CFILE *cfile, const char *esc) { + const struct colors *colors = cfile->colors; + + if (dstrdcat(&cfile->buffer, colors->leftcode) != 0) { + return -1; + } + if (dstrdcat(&cfile->buffer, esc) != 0) { + return -1; + } + if (dstrdcat(&cfile->buffer, colors->rightcode) != 0) { + return -1; + } + + return 0; +} + +/** Reset after an ANSI escape sequence. */ +static int print_reset(CFILE *cfile) { + const struct colors *colors = cfile->colors; + + if (colors->endcode) { + return dstrdcat(&cfile->buffer, colors->endcode); + } else { + return print_esc(cfile, colors->reset); + } +} + +/** Print a string with an optional color. */ +static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t len) { + if (esc) { + if (print_esc(cfile, esc) != 0) { + return -1; + } + } + if (dstrncat(&cfile->buffer, str, len) != 0) { + return -1; + } + if (esc) { + if (print_reset(cfile) != 0) { + return -1; + } + } + + return 0; +} + +/** Find the offset of the first broken path component. */ +static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) { + ssize_t ret = max; + assert(ret >= 0); + + if (bftw_type(ftwbuf, flags) != BFS_ERROR) { + goto out; + } + + char *at_path; + int at_fd; + if (path == ftwbuf->path) { + if (ftwbuf->depth == 0) { + at_fd = AT_FDCWD; + at_path = dstrndup(path, max); + } else { + // The parent must have existed to get here + goto out; + } + } else { + // We're in print_link_target(), so resolve relative to the link's parent directory + at_fd = ftwbuf->at_fd; + if (at_fd == AT_FDCWD && path[0] != '/') { + at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff); + if (at_path && dstrncat(&at_path, path, max) != 0) { + ret = -1; + goto out_path; + } + } else { + at_path = dstrndup(path, max); + } + } + + if (!at_path) { + ret = -1; + goto out; + } + + while (ret > 0) { + if (xfaccessat(at_fd, at_path, F_OK) == 0) { + break; + } + + size_t len = dstrlen(at_path); + while (ret && at_path[len - 1] == '/') { + --len, --ret; + } + while (ret && at_path[len - 1] != '/') { + --len, --ret; + } + + dstresize(&at_path, len); + } + +out_path: + dstrfree(at_path); +out: + return ret; +} + +/** Print the directories leading up to a file. */ +static int print_dirs_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t nameoff) { + const struct colors *colors = cfile->colors; + + ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); + if (broken < 0) { + return -1; + } + + if (broken > 0) { + if (print_colored(cfile, colors->directory, path, broken) != 0) { + return -1; + } + } + + if ((size_t)broken < nameoff) { + const char *color = colors->missing; + if (!color) { + color = colors->orphan; + } + if (print_colored(cfile, color, path + broken, nameoff - broken) != 0) { + return -1; + } + } + + return 0; +} + +/** Print a file name with colors. */ +static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + const char *color = file_color(cfile->colors, name, ftwbuf, flags); + return print_colored(cfile, color, name, strlen(name)); +} + +/** Print a path with colors. */ +static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + size_t nameoff; + if (path == ftwbuf->path) { + nameoff = ftwbuf->nameoff; + } else { + nameoff = xbasename(path) - path; + } + + if (print_dirs_colored(cfile, path, ftwbuf, flags, nameoff) != 0) { + return -1; + } + + return print_name_colored(cfile, path + nameoff, ftwbuf, flags); +} + +/** Print the name of a file with the appropriate colors. */ +static int print_name(CFILE *cfile, const struct BFTW *ftwbuf) { + const char *name = ftwbuf->path + ftwbuf->nameoff; + + const struct colors *colors = cfile->colors; + if (!colors) { + return dstrcat(&cfile->buffer, name); + } + + enum bfs_stat_flags flags = ftwbuf->stat_flags; + if (colors->link_as_target && ftwbuf->type == BFS_LNK) { + flags = BFS_STAT_TRYFOLLOW; + } + + return print_name_colored(cfile, name, ftwbuf, flags); +} + +/** Print the path to a file with the appropriate colors. */ +static int print_path(CFILE *cfile, const struct BFTW *ftwbuf) { + const struct colors *colors = cfile->colors; + if (!colors) { + return dstrcat(&cfile->buffer, ftwbuf->path); + } + + enum bfs_stat_flags flags = ftwbuf->stat_flags; + if (colors->link_as_target && ftwbuf->type == BFS_LNK) { + flags = BFS_STAT_TRYFOLLOW; + } + + return print_path_colored(cfile, ftwbuf->path, ftwbuf, flags); +} + +/** Print a link target with the appropriate colors. */ +static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW); + size_t len = statbuf ? statbuf->size : 0; + + char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len); + if (!target) { + return -1; + } + + int ret; + if (cfile->colors) { + ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW); + } else { + ret = dstrcat(&cfile->buffer, target); + } + + free(target); + return ret; +} + +/** Format some colored output to the buffer. */ +BFS_FORMATTER(2, 3) +static int cbuff(CFILE *cfile, const char *format, ...); + +/** Dump a parsed expression tree, for debugging. */ +static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) { + if (dstrcat(&cfile->buffer, "(") != 0) { + return -1; + } + + const struct bfs_expr *lhs = NULL; + const struct bfs_expr *rhs = NULL; + + if (bfs_expr_has_children(expr)) { + lhs = expr->lhs; + rhs = expr->rhs; + + if (cbuff(cfile, "${red}%s${rs}", expr->argv[0]) < 0) { + return -1; + } + } else { + if (cbuff(cfile, "${blu}%s${rs}", expr->argv[0]) < 0) { + return -1; + } + } + + for (size_t i = 1; i < expr->argc; ++i) { + if (cbuff(cfile, " ${bld}%s${rs}", expr->argv[i]) < 0) { + return -1; + } + } + + if (verbose) { + double rate = 0.0, time = 0.0; + if (expr->evaluations) { + rate = 100.0*expr->successes/expr->evaluations; + time = (1.0e9*expr->elapsed.tv_sec + expr->elapsed.tv_nsec)/expr->evaluations; + } + if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]", + expr->successes, expr->evaluations, rate, time)) { + return -1; + } + } + + if (lhs) { + if (dstrcat(&cfile->buffer, " ") != 0) { + return -1; + } + if (print_expr(cfile, lhs, verbose) != 0) { + return -1; + } + } + + if (rhs) { + if (dstrcat(&cfile->buffer, " ") != 0) { + return -1; + } + if (print_expr(cfile, rhs, verbose) != 0) { + return -1; + } + } + + if (dstrcat(&cfile->buffer, ")") != 0) { + return -1; + } + + return 0; +} + +static int cvbuff(CFILE *cfile, const char *format, va_list args) { + const struct colors *colors = cfile->colors; + int error = errno; + + for (const char *i = format; *i; ++i) { + size_t verbatim = strcspn(i, "%$"); + if (dstrncat(&cfile->buffer, i, verbatim) != 0) { + return -1; + } + + i += verbatim; + switch (*i) { + case '%': + switch (*++i) { + case '%': + if (dstrapp(&cfile->buffer, '%') != 0) { + return -1; + } + break; + + case 'c': + if (dstrapp(&cfile->buffer, va_arg(args, int)) != 0) { + return -1; + } + break; + + case 'd': + if (dstrcatf(&cfile->buffer, "%d", va_arg(args, int)) != 0) { + return -1; + } + break; + + case 'g': + if (dstrcatf(&cfile->buffer, "%g", va_arg(args, double)) != 0) { + return -1; + } + break; + + case 's': + if (dstrcat(&cfile->buffer, va_arg(args, const char *)) != 0) { + return -1; + } + break; + + case 'z': + ++i; + if (*i != 'u') { + goto invalid; + } + if (dstrcatf(&cfile->buffer, "%zu", va_arg(args, size_t)) != 0) { + return -1; + } + break; + + case 'm': + if (dstrcat(&cfile->buffer, strerror(error)) != 0) { + return -1; + } + break; + + case 'p': + switch (*++i) { + case 'F': + if (print_name(cfile, va_arg(args, const struct BFTW *)) != 0) { + return -1; + } + break; + + case 'P': + if (print_path(cfile, va_arg(args, const struct BFTW *)) != 0) { + return -1; + } + break; + + case 'L': + if (print_link_target(cfile, va_arg(args, const struct BFTW *)) != 0) { + return -1; + } + break; + + case 'e': + if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false) != 0) { + return -1; + } + break; + case 'E': + if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true) != 0) { + return -1; + } + break; + + default: + goto invalid; + } + + break; + + default: + goto invalid; + } + break; + + case '$': + switch (*++i) { + case '$': + if (dstrapp(&cfile->buffer, '$') != 0) { + return -1; + } + break; + + case '{': { + ++i; + const char *end = strchr(i, '}'); + if (!end) { + goto invalid; + } + if (!colors) { + i = end; + break; + } + + size_t len = end - i; + char name[len + 1]; + memcpy(name, i, len); + name[len] = '\0'; + + char **esc = get_color(colors, name); + if (!esc) { + goto invalid; + } + if (*esc) { + if (print_esc(cfile, *esc) != 0) { + return -1; + } + } + + i = end; + break; + } + + default: + goto invalid; + } + break; + + default: + return 0; + } + } + + return 0; + +invalid: + assert(!"Invalid format string"); + errno = EINVAL; + return -1; +} + +static int cbuff(CFILE *cfile, const char *format, ...) { + va_list args; + va_start(args, format); + int ret = cvbuff(cfile, format, args); + va_end(args); + return ret; +} + +int cvfprintf(CFILE *cfile, const char *format, va_list args) { + assert(dstrlen(cfile->buffer) == 0); + + int ret = -1; + if (cvbuff(cfile, format, args) == 0) { + size_t len = dstrlen(cfile->buffer); + if (fwrite(cfile->buffer, 1, len, cfile->file) == len) { + ret = 0; + } + } + + dstresize(&cfile->buffer, 0); + return ret; +} + +int cfprintf(CFILE *cfile, const char *format, ...) { + va_list args; + va_start(args, format); + int ret = cvfprintf(cfile, format, args); + va_end(args); + return ret; +} diff --git a/src/color.h b/src/color.h new file mode 100644 index 0000000..edf1ef7 --- /dev/null +++ b/src/color.h @@ -0,0 +1,120 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2015-2021 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. * + ****************************************************************************/ + +/** + * Utilities for colored output on ANSI terminals. + */ + +#ifndef BFS_COLOR_H +#define BFS_COLOR_H + +#include "util.h" +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> + +/** + * A color scheme. + */ +struct colors; + +/** + * Parse a color table. + * + * @return The parsed color table. + */ +struct colors *parse_colors(void); + +/** + * Free a color table. + * + * @param colors + * The color table to free. + */ +void free_colors(struct colors *colors); + +/** + * A file/stream with associated colors. + */ +typedef struct CFILE { + /** The underlying file/stream. */ + FILE *file; + /** The color table to use, if any. */ + const struct colors *colors; + /** A buffer for colored formatting. */ + char *buffer; + /** Whether to close the underlying stream. */ + bool close; +} CFILE; + +/** + * Wrap an existing file into a colored stream. + * + * @param file + * The underlying file. + * @param colors + * The color table to use if file is a TTY. + * @param close + * Whether to close the underlying stream when this stream is closed. + * @return + * A colored wrapper around file. + */ +CFILE *cfwrap(FILE *file, const struct colors *colors, bool close); + +/** + * Close a colored file. + * + * @param cfile + * The colored file to close. + * @return + * 0 on success, -1 on failure. + */ +int cfclose(CFILE *cfile); + +/** + * Colored, formatted output. + * + * @param cfile + * The colored stream to print to. + * @param format + * A printf()-style format string, supporting these format specifiers: + * + * %c: A single character + * %d: An integer + * %g: A double + * %s: A string + * %zu: A size_t + * %m: strerror(errno) + * %pF: A colored file name, from a const struct BFTW * argument + * %pP: A colored file path, from a const struct BFTW * argument + * %pL: A colored link target, from a const struct BFTW * argument + * %pe: Dump a const struct bfs_expr *, for debugging. + * %pE: Dump a const struct bfs_expr * in verbose form, for debugging. + * %%: A literal '%' + * ${cc}: Change the color to 'cc' + * $$: A literal '$' + * @return + * 0 on success, -1 on failure. + */ +BFS_FORMATTER(2, 3) +int cfprintf(CFILE *cfile, const char *format, ...); + +/** + * cfprintf() variant that takes a va_list. + */ +int cvfprintf(CFILE *cfile, const char *format, va_list args); + +#endif // BFS_COLOR_H diff --git a/src/ctx.c b/src/ctx.c new file mode 100644 index 0000000..8ba2f38 --- /dev/null +++ b/src/ctx.c @@ -0,0 +1,311 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +#include "ctx.h" +#include "color.h" +#include "darray.h" +#include "diag.h" +#include "expr.h" +#include "mtab.h" +#include "pwcache.h" +#include "stat.h" +#include "trie.h" +#include <assert.h> +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> + +const char *debug_flag_name(enum debug_flags flag) { + switch (flag) { + case DEBUG_COST: + return "cost"; + case DEBUG_EXEC: + return "exec"; + case DEBUG_OPT: + return "opt"; + case DEBUG_RATES: + return "rates"; + case DEBUG_SEARCH: + return "search"; + case DEBUG_STAT: + return "stat"; + case DEBUG_TREE: + return "tree"; + + case DEBUG_ALL: + break; + } + + assert(!"Unrecognized debug flag"); + return "???"; +} + +struct bfs_ctx *bfs_ctx_new(void) { + struct bfs_ctx *ctx = malloc(sizeof(*ctx)); + if (!ctx) { + return NULL; + } + + ctx->argv = NULL; + ctx->paths = NULL; + ctx->expr = NULL; + ctx->exclude = NULL; + + ctx->mindepth = 0; + ctx->maxdepth = INT_MAX; + ctx->flags = BFTW_RECOVER; + ctx->strategy = BFTW_BFS; + ctx->optlevel = 3; + ctx->debug = 0; + ctx->ignore_races = false; + ctx->posixly_correct = false; + ctx->status = false; + ctx->unique = false; + ctx->warn = false; + ctx->xargs_safe = false; + + ctx->colors = NULL; + ctx->colors_error = 0; + ctx->cout = NULL; + ctx->cerr = NULL; + + ctx->users = NULL; + ctx->users_error = 0; + ctx->groups = NULL; + ctx->groups_error = 0; + + ctx->mtab = NULL; + ctx->mtab_error = 0; + + trie_init(&ctx->files); + ctx->nfiles = 0; + + struct rlimit rl; + if (getrlimit(RLIMIT_NOFILE, &rl) == 0) { + ctx->nofile_soft = rl.rlim_cur; + ctx->nofile_hard = rl.rlim_max; + } else { + ctx->nofile_soft = 1024; + ctx->nofile_hard = RLIM_INFINITY; + } + + return ctx; +} + +const struct bfs_users *bfs_ctx_users(const struct bfs_ctx *ctx) { + struct bfs_ctx *mut = (struct bfs_ctx *)ctx; + + if (mut->users_error) { + errno = mut->users_error; + } else if (!mut->users) { + mut->users = bfs_users_parse(); + if (!mut->users) { + mut->users_error = errno; + } + } + + return mut->users; +} + +const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx) { + struct bfs_ctx *mut = (struct bfs_ctx *)ctx; + + if (mut->groups_error) { + errno = mut->groups_error; + } else if (!mut->groups) { + mut->groups = bfs_groups_parse(); + if (!mut->groups) { + mut->groups_error = errno; + } + } + + return mut->groups; +} + +const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx) { + struct bfs_ctx *mut = (struct bfs_ctx *)ctx; + + if (mut->mtab_error) { + errno = mut->mtab_error; + } else if (!mut->mtab) { + mut->mtab = bfs_mtab_parse(); + if (!mut->mtab) { + mut->mtab_error = errno; + } + } + + return mut->mtab; +} + +/** + * An open file tracked by the bfs context. + */ +struct bfs_ctx_file { + /** The file itself. */ + CFILE *cfile; + /** The path to the file (for diagnostics). */ + const char *path; +}; + +CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) { + struct bfs_stat sb; + if (bfs_stat(fileno(cfile->file), NULL, 0, &sb) != 0) { + return NULL; + } + + bfs_file_id id; + bfs_stat_id(&sb, &id); + + struct trie_leaf *leaf = trie_insert_mem(&ctx->files, id, sizeof(id)); + if (!leaf) { + return NULL; + } + + struct bfs_ctx_file *ctx_file = leaf->value; + if (ctx_file) { + ctx_file->path = path; + return ctx_file->cfile; + } + + leaf->value = ctx_file = malloc(sizeof(*ctx_file)); + if (!ctx_file) { + trie_remove(&ctx->files, leaf); + return NULL; + } + + ctx_file->cfile = cfile; + ctx_file->path = path; + + if (cfile != ctx->cout && cfile != ctx->cerr) { + ++ctx->nfiles; + } + + return cfile; +} + +void bfs_ctx_flush(const struct bfs_ctx *ctx) { + // Before executing anything, flush all open streams. This ensures that + // - the user sees everything relevant before an -ok[dir] prompt + // - output from commands is interleaved consistently with bfs + // - executed commands can rely on I/O from other bfs actions + // + // We do not check errors here, but they will be caught at cleanup time + // with ferror(). + fflush(NULL); +} + +/** Flush a file and report any errors. */ +static int bfs_ctx_fflush(CFILE *cfile) { + int ret = 0, error = 0; + if (ferror(cfile->file)) { + ret = -1; + error = EIO; + } + if (fflush(cfile->file) != 0) { + ret = -1; + error = errno; + } + + errno = error; + return ret; +} + +/** Close a file tracked by the bfs context. */ +static int bfs_ctx_fclose(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) { + CFILE *cfile = ctx_file->cfile; + + if (cfile == ctx->cout) { + // Will be checked later + return 0; + } else if (cfile == ctx->cerr) { + // Writes to stderr are allowed to fail silently, unless the same file was used by + // -fprint, -fls, etc. + if (ctx_file->path) { + return bfs_ctx_fflush(cfile); + } else { + return 0; + } + } + + int ret = 0, error = 0; + if (ferror(cfile->file)) { + ret = -1; + error = EIO; + } + if (cfclose(cfile) != 0) { + ret = -1; + error = errno; + } + + errno = error; + return ret; +} + +int bfs_ctx_free(struct bfs_ctx *ctx) { + int ret = 0; + + if (ctx) { + CFILE *cout = ctx->cout; + CFILE *cerr = ctx->cerr; + + bfs_expr_free(ctx->exclude); + bfs_expr_free(ctx->expr); + + bfs_mtab_free(ctx->mtab); + + bfs_groups_free(ctx->groups); + bfs_users_free(ctx->users); + + struct trie_leaf *leaf; + while ((leaf = trie_first_leaf(&ctx->files))) { + struct bfs_ctx_file *ctx_file = leaf->value; + + if (bfs_ctx_fclose(ctx, ctx_file) != 0) { + if (cerr) { + bfs_error(ctx, "'%s': %m.\n", ctx_file->path); + } + ret = -1; + } + + free(ctx_file); + trie_remove(&ctx->files, leaf); + } + trie_destroy(&ctx->files); + + if (cout && bfs_ctx_fflush(cout) != 0) { + if (cerr) { + bfs_error(ctx, "standard output: %m.\n"); + } + ret = -1; + } + + cfclose(cout); + cfclose(cerr); + + free_colors(ctx->colors); + + for (size_t i = 0; i < darray_length(ctx->paths); ++i) { + free((char *)ctx->paths[i]); + } + darray_free(ctx->paths); + + free(ctx->argv); + free(ctx); + } + + return ret; +} diff --git a/src/ctx.h b/src/ctx.h new file mode 100644 index 0000000..3ad7f85 --- /dev/null +++ b/src/ctx.h @@ -0,0 +1,212 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +/** + * bfs execution context. + */ + +#ifndef BFS_CTX_H +#define BFS_CTX_H + +#include "bftw.h" +#include "trie.h" +#include <stdbool.h> +#include <sys/resource.h> + +/** + * Various debugging flags. + */ +enum debug_flags { + /** Print cost estimates. */ + DEBUG_COST = 1 << 0, + /** Print executed command details. */ + DEBUG_EXEC = 1 << 1, + /** Print optimization details. */ + DEBUG_OPT = 1 << 2, + /** Print rate information. */ + DEBUG_RATES = 1 << 3, + /** Trace the filesystem traversal. */ + DEBUG_SEARCH = 1 << 4, + /** Trace all stat() calls. */ + DEBUG_STAT = 1 << 5, + /** Print the parse tree. */ + DEBUG_TREE = 1 << 6, + /** All debug flags. */ + DEBUG_ALL = (1 << 7) - 1, +}; + +/** + * Convert a debug flag to a string. + */ +const char *debug_flag_name(enum debug_flags flag); + +/** + * The execution context for bfs. + */ +struct bfs_ctx { + /** The number of command line arguments. */ + size_t argc; + /** The unparsed command line arguments. */ + char **argv; + + /** The root paths. */ + const char **paths; + /** The main command line expression. */ + struct bfs_expr *expr; + /** An expression for files to filter out. */ + struct bfs_expr *exclude; + + /** -mindepth option. */ + int mindepth; + /** -maxdepth option. */ + int maxdepth; + + /** bftw() flags. */ + enum bftw_flags flags; + /** bftw() search strategy. */ + enum bftw_strategy strategy; + + /** Optimization level (-O). */ + int optlevel; + /** Debugging flags (-D). */ + enum debug_flags debug; + /** Whether to ignore deletions that race with bfs (-ignore_readdir_race). */ + bool ignore_races; + /** Whether to follow POSIXisms more closely ($POSIXLY_CORRECT). */ + bool posixly_correct; + /** Whether to show a status bar (-status). */ + bool status; + /** Whether to only return unique files (-unique). */ + bool unique; + /** Whether to print warnings (-warn/-nowarn). */ + bool warn; + /** Whether to only handle paths with xargs-safe characters (-X). */ + bool xargs_safe; + + /** Color data. */ + struct colors *colors; + /** The error that occurred parsing the color table, if any. */ + int colors_error; + /** Colored stdout. */ + struct CFILE *cout; + /** Colored stderr. */ + struct CFILE *cerr; + + /** User table. */ + struct bfs_users *users; + /** The error that occurred parsing the user table, if any. */ + int users_error; + /** Group table. */ + struct bfs_groups *groups; + /** The error that occurred parsing the group table, if any. */ + int groups_error; + + /** Table of mounted file systems. */ + struct bfs_mtab *mtab; + /** The error that occurred parsing the mount table, if any. */ + int mtab_error; + + /** All the files owned by the context. */ + struct trie files; + /** The number of files owned by the context. */ + int nfiles; + + /** The initial RLIMIT_NOFILE soft limit. */ + rlim_t nofile_soft; + /** The initial RLIMIT_NOFILE hard limit. */ + rlim_t nofile_hard; +}; + +/** + * @return + * A new bfs context, or NULL on failure. + */ +struct bfs_ctx *bfs_ctx_new(void); + +/** + * Get the users table. + * + * @param ctx + * The bfs context. + * @return + * The cached users table, or NULL on failure. + */ +const struct bfs_users *bfs_ctx_users(const struct bfs_ctx *ctx); + +/** + * Get the groups table. + * + * @param ctx + * The bfs context. + * @return + * The cached groups table, or NULL on failure. + */ +const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx); + +/** + * Get the mount table. + * + * @param ctx + * The bfs context. + * @return + * The cached mount table, or NULL on failure. + */ +const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx); + +/** + * Deduplicate an opened file. + * + * @param ctx + * The bfs context. + * @param cfile + * The opened file. + * @param path + * The path to the opened file (or NULL for standard streams). + * @return + * If the same file was opened previously, that file is returned. If cfile is a new file, + * cfile itself is returned. If an error occurs, NULL is returned. + */ +struct CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, struct CFILE *cfile, const char *path); + +/** + * Flush any caches for consistency with external processes. + * + * @param ctx + * The bfs context. + */ +void bfs_ctx_flush(const struct bfs_ctx *ctx); + +/** + * Dump the parsed command line. + * + * @param ctx + * The bfs context. + * @param flag + * The -D flag that triggered the dump. + */ +void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag); + +/** + * Free a bfs context. + * + * @param ctx + * The context to free. + * @return + * 0 on success, -1 if any errors occurred. + */ +int bfs_ctx_free(struct bfs_ctx *ctx); + +#endif // BFS_CTX_H diff --git a/src/darray.c b/src/darray.c new file mode 100644 index 0000000..6585d30 --- /dev/null +++ b/src/darray.c @@ -0,0 +1,103 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019-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. * + ****************************************************************************/ + +#include "darray.h" +#include <stdlib.h> +#include <string.h> + +/** + * The darray header. + */ +struct darray { + /** The current capacity of the array, as a count of elements. */ + size_t capacity; + /** The current length of the array. */ + size_t length; + + // The array elements are stored after this header in memory. Not using + // a flexible array member to avoid worrying about strict aliasing. We + // assume that 2*sizeof(size_t) keeps any memory allocation suitably + // aligned for the element type. +}; + +/** Get the header for a darray. */ +static struct darray *darray_header(const void *da) { + return (struct darray *)da - 1; +} + +/** Get the array from a darray header. */ +static char *darray_data(struct darray *header) { + return (char *)(header + 1); +} + +size_t darray_length(const void *da) { + if (da) { + return darray_header(da)->length; + } else { + return 0; + } +} + +void *darray_push(void *da, const void *item, size_t size) { + struct darray *header; + if (da) { + header = darray_header(da); + } else { + header = malloc(sizeof(*header) + size); + if (!header) { + return NULL; + } + header->capacity = 1; + header->length = 0; + } + + size_t capacity = header->capacity; + size_t i = header->length++; + if (i >= capacity) { + capacity *= 2; + header = realloc(header, sizeof(*header) + capacity*size); + if (!header) { + // This failure will be detected by darray_check() + return da; + } + header->capacity = capacity; + } + + char *data = darray_data(header); + memcpy(data + i*size, item, size); + return data; +} + +int darray_check(void *da) { + if (!da) { + return -1; + } + + struct darray *header = darray_header(da); + if (header->length <= header->capacity) { + return 0; + } else { + // realloc() failed in darray_push(), so reset the length and report the failure + header->length = header->capacity; + return -1; + } +} + +void darray_free(void *da) { + if (da) { + free(darray_header(da)); + } +} diff --git a/src/darray.h b/src/darray.h new file mode 100644 index 0000000..4464381 --- /dev/null +++ b/src/darray.h @@ -0,0 +1,110 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019-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. * + ****************************************************************************/ + +/** + * A dynamic array library. + * + * darrays are represented by a simple pointer to the array element type, like + * any other array. Behind the scenes, the capacity and current length of the + * array are stored along with it. NULL is a valid way to initialize an empty + * darray: + * + * int *darray = NULL; + * + * To append an element to a darray, use the DARRAY_PUSH macro: + * + * int e = 42; + * if (DARRAY_PUSH(&darray, &e) != 0) { + * // Report the error... + * } + * + * The length can be retrieved by darray_length(). Iterating over the array + * works like normal arrays: + * + * for (size_t i = 0; i < darray_length(darray); ++i) { + * printf("%d\n", darray[i]); + * } + * + * To free a darray, use darray_free(): + * + * darray_free(darray); + */ + +#ifndef BFS_DARRAY_H +#define BFS_DARRAY_H + +#include <stddef.h> + +/** + * Get the length of a darray. + * + * @param da + * The array in question. + * @return + * The length of the array. + */ +size_t darray_length(const void *da); + +/** + * @internal Use DARRAY_PUSH(). + * + * Push an element into a darray. + * + * @param da + * The array to append to. + * @param item + * The item to append. + * @param size + * The size of the item. + * @return + * The (new) location of the array. + */ +void *darray_push(void *da, const void *item, size_t size); + +/** + * @internal Use DARRAY_PUSH(). + * + * Check if the last darray_push() call failed. + * + * @param da + * The darray to check. + * @return + * 0 on success, -1 on failure. + */ +int darray_check(void *da); + +/** + * Free a darray. + * + * @param da + * The darray to free. + */ +void darray_free(void *da); + +/** + * Push an item into a darray. + * + * @param da + * The array to append to. + * @param item + * A pointer to the item to append. + * @return + * 0 on success, -1 on failure. + */ +#define DARRAY_PUSH(da, item) \ + (darray_check(*(da) = darray_push(*(da), (item), sizeof(**(da) = *(item))))) + +#endif // BFS_DARRAY_H diff --git a/src/diag.c b/src/diag.c new file mode 100644 index 0000000..27848f1 --- /dev/null +++ b/src/diag.c @@ -0,0 +1,233 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019-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. * + ****************************************************************************/ + +#include "diag.h" +#include "ctx.h" +#include "color.h" +#include "expr.h" +#include "util.h" +#include <assert.h> +#include <errno.h> +#include <stdarg.h> + +void bfs_perror(const struct bfs_ctx *ctx, const char *str) { + bfs_error(ctx, "%s: %m.\n", str); +} + +void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) { + va_list args; + va_start(args, format); + bfs_verror(ctx, format, args); + va_end(args); +} + +bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) { + va_list args; + va_start(args, format); + bool ret = bfs_vwarning(ctx, format, args); + va_end(args); + return ret; +} + +bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) { + va_list args; + va_start(args, format); + bool ret = bfs_vdebug(ctx, flag, format, args); + va_end(args); + return ret; +} + +void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args) { + int error = errno; + + bfs_error_prefix(ctx); + + errno = error; + cvfprintf(ctx->cerr, format, args); +} + +bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) { + int error = errno; + + if (bfs_warning_prefix(ctx)) { + errno = error; + cvfprintf(ctx->cerr, format, args); + return true; + } else { + return false; + } +} + +bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args) { + int error = errno; + + if (bfs_debug_prefix(ctx, flag)) { + errno = error; + cvfprintf(ctx->cerr, format, args); + return true; + } else { + return false; + } +} + +void bfs_error_prefix(const struct bfs_ctx *ctx) { + cfprintf(ctx->cerr, "${bld}%s:${rs} ${err}error:${rs} ", xbasename(ctx->argv[0])); +} + +bool bfs_warning_prefix(const struct bfs_ctx *ctx) { + if (ctx->warn) { + cfprintf(ctx->cerr, "${bld}%s:${rs} ${wrn}warning:${rs} ", xbasename(ctx->argv[0])); + return true; + } else { + return false; + } +} + +bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) { + if (ctx->debug & flag) { + cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", xbasename(ctx->argv[0]), debug_flag_name(flag)); + return true; + } else { + return false; + } +} + +/** Recursive part of highlight_expr(). */ +static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool *args) { + if (!expr) { + return false; + } + + bool ret = false; + + if (!expr->synthetic) { + size_t i = expr->argv - ctx->argv; + for (size_t j = 0; j < expr->argc; ++j) { + assert(i + j < ctx->argc); + args[i + j] = true; + ret = true; + } + } + + if (bfs_expr_has_children(expr)) { + ret |= highlight_expr_recursive(ctx, expr->lhs, args); + ret |= highlight_expr_recursive(ctx, expr->rhs, args); + } + + return ret; +} + +/** Highlight an expression in the command line. */ +static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool *args) { + for (size_t i = 0; i < ctx->argc; ++i) { + args[i] = false; + } + + return highlight_expr_recursive(ctx, expr, args); +} + +/** Print a highlighted portion of the command line. */ +static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warning) { + if (warning) { + bfs_warning_prefix(ctx); + } else { + bfs_error_prefix(ctx); + } + + size_t max_argc = 0; + for (size_t i = 0; i < ctx->argc; ++i) { + if (i > 0) { + cfprintf(ctx->cerr, " "); + } + + if (args[i]) { + max_argc = i + 1; + cfprintf(ctx->cerr, "${bld}%s${rs}", ctx->argv[i]); + } else { + cfprintf(ctx->cerr, "%s", ctx->argv[i]); + } + } + + cfprintf(ctx->cerr, "\n"); + + if (warning) { + bfs_warning_prefix(ctx); + } else { + bfs_error_prefix(ctx); + } + + for (size_t i = 0; i < max_argc; ++i) { + if (i > 0) { + if (args[i - 1] && args[i]) { + cfprintf(ctx->cerr, "~"); + } else { + cfprintf(ctx->cerr, " "); + } + } + + if (args[i] && (i == 0 || !args[i - 1])) { + if (warning) { + cfprintf(ctx->cerr, "${wrn}"); + } else { + cfprintf(ctx->cerr, "${err}"); + } + } + + size_t len = xstrwidth(ctx->argv[i]); + for (size_t j = 0; j < len; ++j) { + if (args[i]) { + cfprintf(ctx->cerr, "~"); + } else { + cfprintf(ctx->cerr, " "); + } + } + + if (args[i] && (i + 1 >= max_argc || !args[i + 1])) { + cfprintf(ctx->cerr, "${rs}"); + } + } + + cfprintf(ctx->cerr, "\n"); +} + +void bfs_argv_error(const struct bfs_ctx *ctx, const bool *args) { + bfs_argv_diag(ctx, args, false); +} + +void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr) { + bool args[ctx->argc]; + if (highlight_expr(ctx, expr, args)) { + bfs_argv_error(ctx, args); + } +} + +bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool *args) { + if (!ctx->warn) { + return false; + } + + bfs_argv_diag(ctx, args, true); + return true; +} + +bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr) { + bool args[ctx->argc]; + if (highlight_expr(ctx, expr, args)) { + return bfs_argv_warning(ctx, args); + } + + return false; +} diff --git a/src/diag.h b/src/diag.h new file mode 100644 index 0000000..39129cc --- /dev/null +++ b/src/diag.h @@ -0,0 +1,108 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019-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. * + ****************************************************************************/ + +/** + * Formatters for diagnostic messages. + */ + +#ifndef BFS_DIAG_H +#define BFS_DIAG_H + +#include "ctx.h" +#include "util.h" +#include <stdarg.h> +#include <stdbool.h> + +struct bfs_expr; + +/** + * Like perror(), but decorated like bfs_error(). + */ +void bfs_perror(const struct bfs_ctx *ctx, const char *str); + +/** + * Shorthand for printing error messages. + */ +BFS_FORMATTER(2, 3) +void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); + +/** + * Shorthand for printing warning messages. + * + * @return Whether a warning was printed. + */ +BFS_FORMATTER(2, 3) +bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); + +/** + * Shorthand for printing debug messages. + * + * @return Whether a debug message was printed. + */ +BFS_FORMATTER(3, 4) +bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...); + +/** + * bfs_error() variant that takes a va_list. + */ +void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args); + +/** + * bfs_warning() variant that takes a va_list. + */ +bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args); + +/** + * bfs_debug() variant that takes a va_list. + */ +bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args); + +/** + * Print the error message prefix. + */ +void bfs_error_prefix(const struct bfs_ctx *ctx); + +/** + * Print the warning message prefix. + */ +bool bfs_warning_prefix(const struct bfs_ctx *ctx); + +/** + * Print the debug message prefix. + */ +bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag); + +/** + * Highlight parts of the command line in an error message. + */ +void bfs_argv_error(const struct bfs_ctx *ctx, const bool *args); + +/** + * Highlight parts of an expression in an error message. + */ +void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr); + +/** + * Highlight parts of the command line in a warning message. + */ +bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool *args); + +/** + * Highlight parts of an expression in a warning message. + */ +bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr); + +#endif // BFS_DIAG_H diff --git a/src/dir.c b/src/dir.c new file mode 100644 index 0000000..024e767 --- /dev/null +++ b/src/dir.c @@ -0,0 +1,303 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2021-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. * + ****************************************************************************/ + +#include "dir.h" +#include "util.h" +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#if __linux__ +# include <sys/syscall.h> +#endif // __linux__ + +enum bfs_type bfs_mode_to_type(mode_t mode) { + switch (mode & S_IFMT) { +#ifdef S_IFBLK + case S_IFBLK: + return BFS_BLK; +#endif +#ifdef S_IFCHR + case S_IFCHR: + return BFS_CHR; +#endif +#ifdef S_IFDIR + case S_IFDIR: + return BFS_DIR; +#endif +#ifdef S_IFDOOR + case S_IFDOOR: + return BFS_DOOR; +#endif +#ifdef S_IFIFO + case S_IFIFO: + return BFS_FIFO; +#endif +#ifdef S_IFLNK + case S_IFLNK: + return BFS_LNK; +#endif +#ifdef S_IFPORT + case S_IFPORT: + return BFS_PORT; +#endif +#ifdef S_IFREG + case S_IFREG: + return BFS_REG; +#endif +#ifdef S_IFSOCK + case S_IFSOCK: + return BFS_SOCK; +#endif +#ifdef S_IFWHT + case S_IFWHT: + return BFS_WHT; +#endif + + default: + return BFS_UNKNOWN; + } +} + +#if __linux__ +/** + * This is not defined in the kernel headers for some reason, callers have to + * define it themselves. + */ +struct linux_dirent64 { + ino64_t d_ino; + off64_t d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +// Make the whole allocation 64k +#define BUF_SIZE ((64 << 10) - 8) +#endif + +struct bfs_dir { +#if __linux__ + int fd; + unsigned short pos; + unsigned short size; +#else + DIR *dir; + struct dirent *de; +#endif +}; + +struct bfs_dir *bfs_opendir(int at_fd, const char *at_path) { +#if __linux__ + struct bfs_dir *dir = malloc(sizeof(*dir) + BUF_SIZE); +#else + struct bfs_dir *dir = malloc(sizeof(*dir)); +#endif + if (!dir) { + return NULL; + } + + int fd; + if (at_path) { + fd = openat(at_fd, at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + } else if (at_fd >= 0) { + fd = at_fd; + } else { + free(dir); + errno = EBADF; + return NULL; + } + + if (fd < 0) { + free(dir); + return NULL; + } + +#if __linux__ + dir->fd = fd; + dir->pos = 0; + dir->size = 0; +#else + dir->dir = fdopendir(fd); + if (!dir->dir) { + if (at_path) { + close_quietly(fd); + } + free(dir); + return NULL; + } + + dir->de = NULL; +#endif // __linux__ + + return dir; +} + +int bfs_dirfd(const struct bfs_dir *dir) { +#if __linux__ + return dir->fd; +#else + return dirfd(dir->dir); +#endif +} + +/** Convert a dirent type to a bfs_type. */ +static enum bfs_type translate_type(int d_type) { + switch (d_type) { +#ifdef DT_BLK + case DT_BLK: + return BFS_BLK; +#endif +#ifdef DT_CHR + case DT_CHR: + return BFS_CHR; +#endif +#ifdef DT_DIR + case DT_DIR: + return BFS_DIR; +#endif +#ifdef DT_DOOR + case DT_DOOR: + return BFS_DOOR; +#endif +#ifdef DT_FIFO + case DT_FIFO: + return BFS_FIFO; +#endif +#ifdef DT_LNK + case DT_LNK: + return BFS_LNK; +#endif +#ifdef DT_PORT + case DT_PORT: + return BFS_PORT; +#endif +#ifdef DT_REG + case DT_REG: + return BFS_REG; +#endif +#ifdef DT_SOCK + case DT_SOCK: + return BFS_SOCK; +#endif +#ifdef DT_WHT + case DT_WHT: + return BFS_WHT; +#endif + } + + return BFS_UNKNOWN; +} + +#if !__linux__ +/** Get the type from a struct dirent if it exists, and convert it. */ +static enum bfs_type dirent_type(const struct dirent *de) { +#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN) + return translate_type(de->d_type); +#else + return BFS_UNKNOWN; +#endif +} +#endif + +/** Check if a name is . or .. */ +static bool is_dot(const char *name) { + return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); +} + +int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) { + while (true) { +#if __linux__ + char *buf = (char *)(dir + 1); + + if (dir->pos >= dir->size) { +#if BFS_HAS_FEATURE(memory_sanitizer, false) + // Make sure msan knows the buffer is initialized + memset(buf, 0, BUF_SIZE); +#endif + + ssize_t size = syscall(__NR_getdents64, dir->fd, buf, BUF_SIZE); + if (size <= 0) { + return size; + } + dir->pos = 0; + dir->size = size; + } + + const struct linux_dirent64 *lde = (void *)(buf + dir->pos); + dir->pos += lde->d_reclen; + + if (is_dot(lde->d_name)) { + continue; + } + + if (de) { + de->type = translate_type(lde->d_type); + de->name = lde->d_name; + } + + return 1; +#else // !__linux__ + errno = 0; + dir->de = readdir(dir->dir); + if (dir->de) { + if (is_dot(dir->de->d_name)) { + continue; + } + if (de) { + de->type = dirent_type(dir->de); + de->name = dir->de->d_name; + } + return 1; + } else if (errno != 0) { + return -1; + } else { + return 0; + } +#endif // !__linux__ + } +} + +int bfs_closedir(struct bfs_dir *dir) { +#if __linux__ + int ret = xclose(dir->fd); +#else + int ret = closedir(dir->dir); +#endif + free(dir); + return ret; +} + +int bfs_freedir(struct bfs_dir *dir) { +#if __linux__ + int ret = dir->fd; + free(dir); + return ret; +#elif __FreeBSD__ + int ret = fdclosedir(dir->dir); + free(dir); + return ret; +#else + int ret = dup_cloexec(dirfd(dir->dir)); + bfs_closedir(dir); + return ret; +#endif +} diff --git a/src/dir.h b/src/dir.h new file mode 100644 index 0000000..69344c6 --- /dev/null +++ b/src/dir.h @@ -0,0 +1,124 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2021 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. * + ****************************************************************************/ + +/** + * Directories and their contents. + */ + +#ifndef BFS_DIR_H +#define BFS_DIR_H + +#include <sys/types.h> + +/** + * A directory. + */ +struct bfs_dir; + +/** + * File types. + */ +enum bfs_type { + /** An error occurred for this file. */ + BFS_ERROR = -1, + /** Unknown type. */ + BFS_UNKNOWN, + /** Block device. */ + BFS_BLK, + /** Character device. */ + BFS_CHR, + /** Directory. */ + BFS_DIR, + /** Solaris door. */ + BFS_DOOR, + /** Pipe. */ + BFS_FIFO, + /** Symbolic link. */ + BFS_LNK, + /** Solaris event port. */ + BFS_PORT, + /** Regular file. */ + BFS_REG, + /** Socket. */ + BFS_SOCK, + /** BSD whiteout. */ + BFS_WHT, +}; + +/** + * Convert a bfs_stat() mode to a bfs_type. + */ +enum bfs_type bfs_mode_to_type(mode_t mode); + +/** + * A directory entry. + */ +struct bfs_dirent { + /** The type of this file (possibly unknown). */ + enum bfs_type type; + /** The name of this file. */ + const char *name; +}; + +/** + * Open a directory. + * + * @param at_fd + * The base directory for path resolution. + * @param at_path + * The path of the directory to open, relative to at_fd. Pass NULL to + * open at_fd itself. + * @return + * The opened directory, or NULL on failure. + */ +struct bfs_dir *bfs_opendir(int at_fd, const char *at_path); + +/** + * Get the file descriptor for a directory. + */ +int bfs_dirfd(const struct bfs_dir *dir); + +/** + * Read a directory entry. + * + * @param dir + * The directory to read. + * @param[out] dirent + * The directory entry to populate. + * @return + * 1 on success, 0 on EOF, or -1 on failure. + */ +int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de); + +/** + * Close a directory. + * + * @return + * 0 on success, -1 on failure. + */ +int bfs_closedir(struct bfs_dir *dir); + +/** + * Free a directory, keeping an open file descriptor to it. + * + * @param dir + * The directory to free. + * @return + * The file descriptor on success, or -1 on failure. + */ +int bfs_freedir(struct bfs_dir *dir); + +#endif // BFS_DIR_H diff --git a/src/dstring.c b/src/dstring.c new file mode 100644 index 0000000..f344d09 --- /dev/null +++ b/src/dstring.c @@ -0,0 +1,220 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2016-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. * + ****************************************************************************/ + +#include "dstring.h" +#include <assert.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/** + * The memory representation of a dynamic string. Users get a pointer to data. + */ +struct dstring { + size_t capacity; + size_t length; + char data[]; +}; + +/** Get the string header from the string data pointer. */ +static struct dstring *dstrheader(const char *dstr) { + return (struct dstring *)(dstr - offsetof(struct dstring, data)); +} + +/** Get the correct size for a dstring with the given capacity. */ +static size_t dstrsize(size_t capacity) { + return BFS_FLEX_SIZEOF(struct dstring, data, capacity + 1); +} + +/** Allocate a dstring with the given contents. */ +static char *dstralloc_impl(size_t capacity, size_t length, const char *data) { + // Avoid reallocations for small strings + if (capacity < 7) { + capacity = 7; + } + + struct dstring *header = malloc(dstrsize(capacity)); + if (!header) { + return NULL; + } + + header->capacity = capacity; + header->length = length; + + memcpy(header->data, data, length); + header->data[length] = '\0'; + return header->data; +} + +char *dstralloc(size_t capacity) { + return dstralloc_impl(capacity, 0, ""); +} + +char *dstrdup(const char *str) { + size_t len = strlen(str); + return dstralloc_impl(len, len, str); +} + +char *dstrndup(const char *str, size_t n) { + size_t len = strnlen(str, n); + return dstralloc_impl(len, len, str); +} + +size_t dstrlen(const char *dstr) { + return dstrheader(dstr)->length; +} + +int dstreserve(char **dstr, size_t capacity) { + struct dstring *header = dstrheader(*dstr); + + if (capacity > header->capacity) { + capacity *= 2; + + header = realloc(header, dstrsize(capacity)); + if (!header) { + return -1; + } + header->capacity = capacity; + + *dstr = header->data; + } + + return 0; +} + +int dstresize(char **dstr, size_t length) { + if (dstreserve(dstr, length) != 0) { + return -1; + } + + struct dstring *header = dstrheader(*dstr); + header->length = length; + header->data[length] = '\0'; + + return 0; +} + +/** Common implementation of dstr{cat,ncat,app}. */ +static int dstrcat_impl(char **dest, const char *src, size_t srclen) { + size_t oldlen = dstrlen(*dest); + size_t newlen = oldlen + srclen; + + if (dstresize(dest, newlen) != 0) { + return -1; + } + + memcpy(*dest + oldlen, src, srclen); + return 0; +} + +int dstrcat(char **dest, const char *src) { + return dstrcat_impl(dest, src, strlen(src)); +} + +int dstrncat(char **dest, const char *src, size_t n) { + return dstrcat_impl(dest, src, strnlen(src, n)); +} + +int dstrdcat(char **dest, const char *src) { + return dstrcat_impl(dest, src, dstrlen(src)); +} + +int dstrapp(char **str, char c) { + return dstrcat_impl(str, &c, 1); +} + +char *dstrprintf(const char *format, ...) { + va_list args; + + va_start(args, format); + char *str = dstrvprintf(format, args); + va_end(args); + + return str; +} + +char *dstrvprintf(const char *format, va_list args) { + // Guess a capacity to try to avoid reallocating + char *str = dstralloc(2*strlen(format)); + if (!str) { + return NULL; + } + + if (dstrvcatf(&str, format, args) != 0) { + dstrfree(str); + return NULL; + } + + return str; +} + +int dstrcatf(char **str, const char *format, ...) { + va_list args; + + va_start(args, format); + int ret = dstrvcatf(str, format, args); + va_end(args); + + return ret; +} + +int dstrvcatf(char **str, const char *format, va_list args) { + // Guess a capacity to try to avoid calling vsnprintf() twice + size_t len = dstrlen(*str); + dstreserve(str, len + 2*strlen(format)); + size_t cap = dstrheader(*str)->capacity; + + va_list copy; + va_copy(copy, args); + + char *tail = *str + len; + int ret = vsnprintf(tail, cap - len + 1, format, args); + if (ret < 0) { + goto fail; + } + + size_t tail_len = ret; + if (tail_len > cap - len) { + cap = len + tail_len; + if (dstreserve(str, cap) != 0) { + goto fail; + } + + tail = *str + len; + ret = vsnprintf(tail, tail_len + 1, format, copy); + if (ret < 0 || (size_t)ret != tail_len) { + assert(!"Length of formatted string changed"); + goto fail; + } + } + + va_end(copy); + + struct dstring *header = dstrheader(*str); + header->length += tail_len; + return 0; + +fail: + *tail = '\0'; + return -1; +} + +void dstrfree(char *dstr) { + if (dstr) { + free(dstrheader(dstr)); + } +} diff --git a/src/dstring.h b/src/dstring.h new file mode 100644 index 0000000..54106f3 --- /dev/null +++ b/src/dstring.h @@ -0,0 +1,194 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2016-2020 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. * + ****************************************************************************/ + +/** + * A dynamic string library. + */ + +#ifndef BFS_DSTRING_H +#define BFS_DSTRING_H + +#include "util.h" +#include <stdarg.h> +#include <stddef.h> + +/** + * Allocate a dynamic string. + * + * @param capacity + * The initial capacity of the string. + */ +char *dstralloc(size_t capacity); + +/** + * Create a dynamic copy of a string. + * + * @param str + * The NUL-terminated string to copy. + */ +char *dstrdup(const char *str); + +/** + * Create a length-limited dynamic copy of a string. + * + * @param str + * The string to copy. + * @param n + * The maximum number of characters to copy from str. + */ +char *dstrndup(const char *str, size_t n); + +/** + * Get a dynamic string's length. + * + * @param dstr + * The string to measure. + * @return The length of dstr. + */ +size_t dstrlen(const char *dstr); + +/** + * Reserve some capacity in a dynamic string. + * + * @param dstr + * The dynamic string to preallocate. + * @param capacity + * The new capacity for the string. + * @return 0 on success, -1 on failure. + */ +int dstreserve(char **dstr, size_t capacity); + +/** + * Resize a dynamic string. + * + * @param dstr + * The dynamic string to resize. + * @param length + * The new length for the dynamic string. + * @return 0 on success, -1 on failure. + */ +int dstresize(char **dstr, size_t length); + +/** + * Append to a dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The string to append. + * @return 0 on success, -1 on failure. + */ +int dstrcat(char **dest, const char *src); + +/** + * Append to a dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The string to append. + * @param n + * The maximum number of characters to take from src. + * @return 0 on success, -1 on failure. + */ +int dstrncat(char **dest, const char *src, size_t n); + +/** + * Append a dynamic string to another dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The dynamic string to append. + * @return + * 0 on success, -1 on failure. + */ +int dstrdcat(char **dest, const char *src); + +/** + * Append a single character to a dynamic string. + * + * @param str + * The string to append to. + * @param c + * The character to append. + * @return 0 on success, -1 on failure. + */ +int dstrapp(char **str, char c); + +/** + * Create a dynamic string from a format string. + * + * @param format + * The format string to fill in. + * @param ... + * Any arguments for the format string. + * @return + * The created string, or NULL on failure. + */ +BFS_FORMATTER(1, 2) +char *dstrprintf(const char *format, ...); + +/** + * Create a dynamic string from a format string and a va_list. + * + * @param format + * The format string to fill in. + * @param args + * The arguments for the format string. + * @return + * The created string, or NULL on failure. + */ +char *dstrvprintf(const char *format, va_list args); + +/** + * Format some text onto the end of a dynamic string. + * + * @param str + * The destination dynamic string. + * @param format + * The format string to fill in. + * @param ... + * Any arguments for the format string. + * @return + * 0 on success, -1 on failure. + */ +BFS_FORMATTER(2, 3) +int dstrcatf(char **str, const char *format, ...); + +/** + * Format some text from a va_list onto the end of a dynamic string. + * + * @param str + * The destination dynamic string. + * @param format + * The format string to fill in. + * @param args + * The arguments for the format string. + * @return + * 0 on success, -1 on failure. + */ +int dstrvcatf(char **str, const char *format, va_list args); + +/** + * Free a dynamic string. + * + * @param dstr + * The string to free. + */ +void dstrfree(char *dstr); + +#endif // BFS_DSTRING_H 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; +} diff --git a/src/eval.h b/src/eval.h new file mode 100644 index 0000000..a1bbd2f --- /dev/null +++ b/src/eval.h @@ -0,0 +1,113 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +/** + * The evaluation functions that implement literal expressions like -name, + * -print, etc. + */ + +#ifndef BFS_EVAL_H +#define BFS_EVAL_H + +#include <stdbool.h> + +struct bfs_ctx; +struct bfs_expr; + +/** + * Ephemeral state for evaluating an expression. + */ +struct bfs_eval; + +/** + * Expression evaluation function. + * + * @param expr + * The current expression. + * @param state + * The current evaluation state. + * @return + * The result of the test. + */ +typedef bool bfs_eval_fn(const struct bfs_expr *expr, struct bfs_eval *state); + +/** + * Evaluate the command line. + * + * @param ctx + * The bfs context to evaluate. + * @return + * EXIT_SUCCESS on success, otherwise on failure. + */ +int bfs_eval(const struct bfs_ctx *ctx); + +// Predicate evaluation functions + +bool eval_true(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state); + +bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state); + +bool eval_newer(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_used(const struct bfs_expr *expr, struct bfs_eval *state); + +bool eval_gid(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_uid(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_nogroup(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_nouser(const struct bfs_expr *expr, struct bfs_eval *state); + +bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_flags(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_fstype(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_hidden(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_inum(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_links(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_samefile(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_size(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_sparse(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_type(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state); + +bool eval_lname(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_path(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_regex(const struct bfs_expr *expr, struct bfs_eval *state); + +bool eval_delete(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_exit(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_quit(const struct bfs_expr *expr, struct bfs_eval *state); + +// Operator evaluation functions +bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state); + +#endif // BFS_EVAL_H diff --git a/src/exec.c b/src/exec.c new file mode 100644 index 0000000..0130317 --- /dev/null +++ b/src/exec.c @@ -0,0 +1,715 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-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. * + ****************************************************************************/ + +#include "exec.h" +#include "bftw.h" +#include "ctx.h" +#include "color.h" +#include "diag.h" +#include "dstring.h" +#include "util.h" +#include "xspawn.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/resource.h> +#include <sys/wait.h> +#include <unistd.h> + +/** Print some debugging info. */ +BFS_FORMATTER(2, 3) +static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) { + const struct bfs_ctx *ctx = execbuf->ctx; + + if (!bfs_debug(ctx, DEBUG_EXEC, "${blu}")) { + return; + } + + if (execbuf->flags & BFS_EXEC_CONFIRM) { + fputs("-ok", stderr); + } else { + fputs("-exec", stderr); + } + if (execbuf->flags & BFS_EXEC_CHDIR) { + fputs("dir", stderr); + } + cfprintf(ctx->cerr, "${rs}: "); + + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +/** Determine the size of a single argument, for comparison to arg_max. */ +static size_t bfs_exec_arg_size(const char *arg) { + return sizeof(arg) + strlen(arg) + 1; +} + +/** Even if we can pass a bigger argument list, cap it here. */ +#define BFS_EXEC_ARG_MAX (16 << 20) + +/** Determine the maximum argv size. */ +static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) { + long arg_max = sysconf(_SC_ARG_MAX); + bfs_exec_debug(execbuf, "ARG_MAX: %ld according to sysconf()\n", arg_max); + if (arg_max < 0) { + arg_max = BFS_EXEC_ARG_MAX; + bfs_exec_debug(execbuf, "ARG_MAX: %ld assumed\n", arg_max); + } + + // We have to share space with the environment variables + extern char **environ; + for (char **envp = environ; *envp; ++envp) { + arg_max -= bfs_exec_arg_size(*envp); + } + // Account for the terminating NULL entry + arg_max -= sizeof(char *); + bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after environment variables\n", arg_max); + + // Account for the fixed arguments + for (size_t i = 0; i < execbuf->tmpl_argc - 1; ++i) { + arg_max -= bfs_exec_arg_size(execbuf->tmpl_argv[i]); + } + // Account for the terminating NULL entry + arg_max -= sizeof(char *); + bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after fixed arguments\n", arg_max); + + // Assume arguments are counted with the granularity of a single page, + // so allow a one page cushion to account for rounding up + long page_size = sysconf(_SC_PAGESIZE); + if (page_size < 4096) { + page_size = 4096; + } + arg_max -= page_size; + bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after page cushion\n", arg_max); + + // POSIX recommends an additional 2048 bytes of headroom + arg_max -= 2048; + bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after headroom\n", arg_max); + + if (arg_max < 0) { + arg_max = 0; + } else if (arg_max > BFS_EXEC_ARG_MAX) { + arg_max = BFS_EXEC_ARG_MAX; + } + + bfs_exec_debug(execbuf, "ARG_MAX: %ld final value\n", arg_max); + return arg_max; +} + +/** Highlight part of the command line as an error. */ +static void bfs_exec_parse_error(const struct bfs_ctx *ctx, const struct bfs_exec *execbuf) { + char **argv = execbuf->tmpl_argv - 1; + size_t argc = execbuf->tmpl_argc + 1; + if (argv[argc]) { + ++argc; + } + + bool args[ctx->argc]; + for (size_t i = 0; i < ctx->argc; ++i) { + args[i] = false; + } + + size_t i = argv - ctx->argv; + for (size_t j = 0; j < argc; ++j) { + args[i + j] = true; + } + + bfs_argv_error(ctx, args); +} + +struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs_exec_flags flags) { + struct bfs_exec *execbuf = malloc(sizeof(*execbuf)); + if (!execbuf) { + bfs_perror(ctx, "malloc()"); + goto fail; + } + + execbuf->flags = flags; + execbuf->ctx = ctx; + execbuf->tmpl_argv = argv + 1; + execbuf->tmpl_argc = 0; + execbuf->argv = NULL; + execbuf->argc = 0; + execbuf->argv_cap = 0; + execbuf->arg_size = 0; + execbuf->arg_max = 0; + execbuf->arg_min = 0; + execbuf->wd_fd = -1; + execbuf->wd_path = NULL; + execbuf->wd_len = 0; + execbuf->ret = 0; + + while (true) { + const char *arg = execbuf->tmpl_argv[execbuf->tmpl_argc]; + if (!arg) { + if (execbuf->flags & BFS_EXEC_CONFIRM) { + bfs_exec_parse_error(ctx, execbuf); + bfs_error(ctx, "Expected '... ;'.\n"); + } else { + bfs_exec_parse_error(ctx, execbuf); + bfs_error(ctx, "Expected '... ;' or '... {} +'.\n"); + } + goto fail; + } else if (strcmp(arg, ";") == 0) { + break; + } else if (strcmp(arg, "+") == 0) { + const char *prev = execbuf->tmpl_argv[execbuf->tmpl_argc - 1]; + if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(prev, "{}") == 0) { + execbuf->flags |= BFS_EXEC_MULTI; + break; + } + } + + ++execbuf->tmpl_argc; + } + + if (execbuf->tmpl_argc == 0) { + bfs_exec_parse_error(ctx, execbuf); + bfs_error(ctx, "Missing command.\n"); + goto fail; + } + + execbuf->argv_cap = execbuf->tmpl_argc + 1; + execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv)); + if (!execbuf->argv) { + bfs_perror(ctx, "malloc()"); + goto fail; + } + + if (execbuf->flags & BFS_EXEC_MULTI) { + for (size_t i = 0; i < execbuf->tmpl_argc - 1; ++i) { + char *arg = execbuf->tmpl_argv[i]; + if (strstr(arg, "{}")) { + bfs_exec_parse_error(ctx, execbuf); + bfs_error(ctx, "Only one '{}' is supported.\n"); + goto fail; + } + execbuf->argv[i] = arg; + } + execbuf->argc = execbuf->tmpl_argc - 1; + + execbuf->arg_max = bfs_exec_arg_max(execbuf); + execbuf->arg_min = execbuf->arg_max; + } + + return execbuf; + +fail: + bfs_exec_free(execbuf); + return NULL; +} + +/** Format the current path for use as a command line argument. */ +static char *bfs_exec_format_path(const struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + if (!(execbuf->flags & BFS_EXEC_CHDIR)) { + return strdup(ftwbuf->path); + } + + const char *name = ftwbuf->path + ftwbuf->nameoff; + + if (name[0] == '/') { + // Must be a root path ("/", "//", etc.) + return strdup(name); + } + + // For compatibility with GNU find, use './name' instead of just 'name' + char *path = malloc(2 + strlen(name) + 1); + if (!path) { + return NULL; + } + + strcpy(path, "./"); + strcpy(path + 2, name); + + return path; +} + +/** Format an argument, expanding "{}" to the current path. */ +static char *bfs_exec_format_arg(char *arg, const char *path) { + char *match = strstr(arg, "{}"); + if (!match) { + return arg; + } + + char *ret = dstralloc(0); + if (!ret) { + return NULL; + } + + char *last = arg; + do { + if (dstrncat(&ret, last, match - last) != 0) { + goto err; + } + if (dstrcat(&ret, path) != 0) { + goto err; + } + + last = match + 2; + match = strstr(last, "{}"); + } while (match); + + if (dstrcat(&ret, last) != 0) { + goto err; + } + + return ret; + +err: + dstrfree(ret); + return NULL; +} + +/** Free a formatted argument. */ +static void bfs_exec_free_arg(char *arg, const char *tmpl) { + if (arg != tmpl) { + dstrfree(arg); + } +} + +/** Open a file to use as the working directory. */ +static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + assert(execbuf->wd_fd < 0); + assert(!execbuf->wd_path); + + if (ftwbuf->at_fd != AT_FDCWD) { + // Rely on at_fd being the immediate parent + assert(ftwbuf->at_path == xbasename(ftwbuf->at_path)); + + execbuf->wd_fd = ftwbuf->at_fd; + if (!(execbuf->flags & BFS_EXEC_MULTI)) { + return 0; + } + + execbuf->wd_fd = dup_cloexec(execbuf->wd_fd); + if (execbuf->wd_fd < 0) { + return -1; + } + } + + execbuf->wd_len = ftwbuf->nameoff; + if (execbuf->wd_len == 0) { + if (ftwbuf->path[0] == '/') { + ++execbuf->wd_len; + } else { + // The path is something like "foo", so we're already in the right directory + return 0; + } + } + + execbuf->wd_path = strndup(ftwbuf->path, execbuf->wd_len); + if (!execbuf->wd_path) { + return -1; + } + + if (execbuf->wd_fd < 0) { + execbuf->wd_fd = open(execbuf->wd_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + } + + if (execbuf->wd_fd < 0) { + return -1; + } + + return 0; +} + +/** Close the working directory. */ +static void bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + if (execbuf->wd_fd >= 0) { + if (!ftwbuf || execbuf->wd_fd != ftwbuf->at_fd) { + xclose(execbuf->wd_fd); + } + execbuf->wd_fd = -1; + } + + if (execbuf->wd_path) { + free(execbuf->wd_path); + execbuf->wd_path = NULL; + execbuf->wd_len = 0; + } +} + +/** Actually spawn the process. */ +static int bfs_exec_spawn(const struct bfs_exec *execbuf) { + if (execbuf->flags & BFS_EXEC_CONFIRM) { + for (size_t i = 0; i < execbuf->argc; ++i) { + if (fprintf(stderr, "%s ", execbuf->argv[i]) < 0) { + return -1; + } + } + if (fprintf(stderr, "? ") < 0) { + return -1; + } + + if (ynprompt() <= 0) { + errno = 0; + return -1; + } + } + + // Flush cached state for consistency with the external process + bfs_ctx_flush(execbuf->ctx); + + if (execbuf->flags & BFS_EXEC_MULTI) { + bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments] (size %zu)\n", + execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size); + } else { + bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments]\n", execbuf->argv[0], execbuf->argc - 1); + } + + pid_t pid = -1; + int error; + + struct bfs_spawn ctx; + if (bfs_spawn_init(&ctx) != 0) { + return -1; + } + + if (bfs_spawn_setflags(&ctx, BFS_SPAWN_USEPATH) != 0) { + goto fail; + } + + // Reset RLIMIT_NOFILE, to avoid breaking applications that use select() + struct rlimit rl = { + .rlim_cur = execbuf->ctx->nofile_soft, + .rlim_max = execbuf->ctx->nofile_hard, + }; + if (bfs_spawn_addsetrlimit(&ctx, RLIMIT_NOFILE, &rl) != 0) { + goto fail; + } + + if (execbuf->wd_fd >= 0) { + if (bfs_spawn_addfchdir(&ctx, execbuf->wd_fd) != 0) { + goto fail; + } + } + + pid = bfs_spawn(execbuf->argv[0], &ctx, execbuf->argv, NULL); +fail: + error = errno; + bfs_spawn_destroy(&ctx); + if (pid < 0) { + errno = error; + return -1; + } + + int wstatus; + if (waitpid(pid, &wstatus, 0) < 0) { + return -1; + } + + int ret = -1; + + if (WIFEXITED(wstatus)) { + int status = WEXITSTATUS(wstatus); + if (status == EXIT_SUCCESS) { + ret = 0; + } else { + bfs_exec_debug(execbuf, "Command '%s' failed with status %d\n", execbuf->argv[0], status); + } + } else if (WIFSIGNALED(wstatus)) { + int sig = WTERMSIG(wstatus); + const char *str = strsignal(sig); + if (!str) { + str = "unknown"; + } + bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str); + } else { + bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]); + } + + errno = 0; + return ret; +} + +/** exec() a command for a single file. */ +static int bfs_exec_single(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + int ret = -1, error = 0; + + char *path = bfs_exec_format_path(execbuf, ftwbuf); + if (!path) { + goto out; + } + + size_t i; + for (i = 0; i < execbuf->tmpl_argc; ++i) { + execbuf->argv[i] = bfs_exec_format_arg(execbuf->tmpl_argv[i], path); + if (!execbuf->argv[i]) { + goto out_free; + } + } + execbuf->argv[i] = NULL; + execbuf->argc = i; + + if (execbuf->flags & BFS_EXEC_CHDIR) { + if (bfs_exec_openwd(execbuf, ftwbuf) != 0) { + goto out_free; + } + } + + ret = bfs_exec_spawn(execbuf); + +out_free: + error = errno; + + bfs_exec_closewd(execbuf, ftwbuf); + + for (size_t j = 0; j < i; ++j) { + bfs_exec_free_arg(execbuf->argv[j], execbuf->tmpl_argv[j]); + } + + free(path); + + errno = error; + +out: + return ret; +} + +/** Check if any arguments remain in the buffer. */ +static bool bfs_exec_args_remain(const struct bfs_exec *execbuf) { + return execbuf->argc >= execbuf->tmpl_argc; +} + +/** Compute the current ARG_MAX estimate for binary search. */ +static size_t bfs_exec_estimate_max(const struct bfs_exec *execbuf) { + size_t min = execbuf->arg_min; + size_t max = execbuf->arg_max; + return min + (max - min)/2; +} + +/** Update the ARG_MAX lower bound from a successful execution. */ +static void bfs_exec_update_min(struct bfs_exec *execbuf) { + if (execbuf->arg_size > execbuf->arg_min) { + execbuf->arg_min = execbuf->arg_size; + + // Don't let min exceed max + if (execbuf->arg_min > execbuf->arg_max) { + execbuf->arg_min = execbuf->arg_max; + } + + size_t estimate = bfs_exec_estimate_max(execbuf); + bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n", + execbuf->arg_min, execbuf->arg_max, estimate); + } +} + +/** Update the ARG_MAX upper bound from a failed execution. */ +static size_t bfs_exec_update_max(struct bfs_exec *execbuf) { + bfs_exec_debug(execbuf, "Got E2BIG, shrinking argument list...\n"); + + size_t size = execbuf->arg_size; + if (size <= execbuf->arg_min) { + // Lower bound was wrong, restart binary search. + execbuf->arg_min = 0; + } + + // Trim a fraction off the max size to avoid repeated failures near the + // top end of the working range + size -= size/16; + if (size < execbuf->arg_max) { + execbuf->arg_max = size; + + // Don't let min exceed max + if (execbuf->arg_min > execbuf->arg_max) { + execbuf->arg_min = execbuf->arg_max; + } + } + + // Binary search for a more precise bound + size_t estimate = bfs_exec_estimate_max(execbuf); + bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n", + execbuf->arg_min, execbuf->arg_max, estimate); + return estimate; +} + +/** Execute the pending command from a BFS_EXEC_MULTI execbuf. */ +static int bfs_exec_flush(struct bfs_exec *execbuf) { + int ret = 0, error = 0; + + size_t orig_argc = execbuf->argc; + while (bfs_exec_args_remain(execbuf)) { + execbuf->argv[execbuf->argc] = NULL; + ret = bfs_exec_spawn(execbuf); + error = errno; + if (ret == 0) { + bfs_exec_update_min(execbuf); + break; + } else if (error != E2BIG) { + break; + } + + // Try to recover from E2BIG by trying fewer and fewer arguments + // until they fit + size_t new_max = bfs_exec_update_max(execbuf); + while (execbuf->arg_size > new_max) { + execbuf->argv[execbuf->argc] = execbuf->argv[execbuf->argc - 1]; + execbuf->arg_size -= bfs_exec_arg_size(execbuf->argv[execbuf->argc]); + --execbuf->argc; + } + } + + size_t new_argc = execbuf->argc; + for (size_t i = execbuf->tmpl_argc - 1; i < new_argc; ++i) { + free(execbuf->argv[i]); + } + execbuf->argc = execbuf->tmpl_argc - 1; + execbuf->arg_size = 0; + + if (new_argc < orig_argc) { + // If we recovered from E2BIG, there are unused arguments at the + // end of the list + for (size_t i = new_argc + 1; i <= orig_argc; ++i) { + if (error == 0) { + execbuf->argv[execbuf->argc] = execbuf->argv[i]; + execbuf->arg_size += bfs_exec_arg_size(execbuf->argv[execbuf->argc]); + ++execbuf->argc; + } else { + free(execbuf->argv[i]); + } + } + } + + errno = error; + return ret; +} + +/** Check if we need to flush the execbuf because we're changing directories. */ +static bool bfs_exec_changed_dirs(const struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + if (execbuf->flags & BFS_EXEC_CHDIR) { + if (ftwbuf->nameoff > execbuf->wd_len + || (execbuf->wd_path && strncmp(ftwbuf->path, execbuf->wd_path, execbuf->wd_len) != 0)) { + bfs_exec_debug(execbuf, "Changed directories, executing buffered command\n"); + return true; + } + } + + return false; +} + +/** Check if we need to flush the execbuf because we're too big. */ +static bool bfs_exec_would_overflow(const struct bfs_exec *execbuf, const char *arg) { + size_t arg_max = bfs_exec_estimate_max(execbuf); + size_t next_size = execbuf->arg_size + bfs_exec_arg_size(arg); + if (next_size > arg_max) { + bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n", + next_size, arg_max); + return true; + } + + return false; +} + +/** Push a new argument to a BFS_EXEC_MULTI execbuf. */ +static int bfs_exec_push(struct bfs_exec *execbuf, char *arg) { + execbuf->argv[execbuf->argc] = arg; + + if (execbuf->argc + 1 >= execbuf->argv_cap) { + size_t cap = 2*execbuf->argv_cap; + char **argv = realloc(execbuf->argv, cap*sizeof(*argv)); + if (!argv) { + return -1; + } + execbuf->argv = argv; + execbuf->argv_cap = cap; + } + + ++execbuf->argc; + execbuf->arg_size += bfs_exec_arg_size(arg); + return 0; +} + +/** Handle a new path for a BFS_EXEC_MULTI execbuf. */ +static int bfs_exec_multi(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + int ret = 0; + + char *arg = bfs_exec_format_path(execbuf, ftwbuf); + if (!arg) { + ret = -1; + goto out; + } + + if (bfs_exec_changed_dirs(execbuf, ftwbuf)) { + while (bfs_exec_args_remain(execbuf)) { + ret |= bfs_exec_flush(execbuf); + } + bfs_exec_closewd(execbuf, ftwbuf); + } else if (bfs_exec_would_overflow(execbuf, arg)) { + ret |= bfs_exec_flush(execbuf); + } + + if ((execbuf->flags & BFS_EXEC_CHDIR) && execbuf->wd_fd < 0) { + if (bfs_exec_openwd(execbuf, ftwbuf) != 0) { + ret = -1; + goto out_arg; + } + } + + if (bfs_exec_push(execbuf, arg) != 0) { + ret = -1; + goto out_arg; + } + + // arg will get cleaned up later by bfs_exec_flush() + goto out; + +out_arg: + free(arg); +out: + return ret; +} + +int bfs_exec(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { + if (execbuf->flags & BFS_EXEC_MULTI) { + if (bfs_exec_multi(execbuf, ftwbuf) == 0) { + errno = 0; + } else { + execbuf->ret = -1; + } + // -exec ... + never returns false + return 0; + } else { + return bfs_exec_single(execbuf, ftwbuf); + } +} + +int bfs_exec_finish(struct bfs_exec *execbuf) { + if (execbuf->flags & BFS_EXEC_MULTI) { + bfs_exec_debug(execbuf, "Finishing execution, executing buffered command\n"); + while (bfs_exec_args_remain(execbuf)) { + execbuf->ret |= bfs_exec_flush(execbuf); + } + if (execbuf->ret != 0) { + bfs_exec_debug(execbuf, "One or more executions of '%s' failed\n", execbuf->argv[0]); + } + } + return execbuf->ret; +} + +void bfs_exec_free(struct bfs_exec *execbuf) { + if (execbuf) { + bfs_exec_closewd(execbuf, NULL); + free(execbuf->argv); + free(execbuf); + } +} diff --git a/src/exec.h b/src/exec.h new file mode 100644 index 0000000..a3e3c71 --- /dev/null +++ b/src/exec.h @@ -0,0 +1,121 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-2020 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 -exec/-execdir/-ok/-okdir. + */ + +#ifndef BFS_EXEC_H +#define BFS_EXEC_H + +#include <stddef.h> + +struct BFTW; +struct bfs_ctx; + +/** + * Flags for the -exec actions. + */ +enum bfs_exec_flags { + /** Prompt the user before executing (-ok, -okdir). */ + BFS_EXEC_CONFIRM = 1 << 0, + /** Run the command in the file's parent directory (-execdir, -okdir). */ + BFS_EXEC_CHDIR = 1 << 1, + /** Pass multiple files at once to the command (-exec ... {} +). */ + BFS_EXEC_MULTI = 1 << 2, +}; + +/** + * Buffer for a command line to be executed. + */ +struct bfs_exec { + /** Flags for this exec buffer. */ + enum bfs_exec_flags flags; + + /** The bfs context. */ + const struct bfs_ctx *ctx; + /** Command line template. */ + char **tmpl_argv; + /** Command line template size. */ + size_t tmpl_argc; + + /** The built command line. */ + char **argv; + /** Number of command line arguments. */ + size_t argc; + /** Capacity of argv. */ + size_t argv_cap; + + /** Current size of all arguments. */ + size_t arg_size; + /** Maximum arg_size before E2BIG. */ + size_t arg_max; + /** Lower bound for arg_max. */ + size_t arg_min; + + /** A file descriptor for the working directory, for BFS_EXEC_CHDIR. */ + int wd_fd; + /** The path to the working directory, for BFS_EXEC_CHDIR. */ + char *wd_path; + /** Length of the working directory path. */ + size_t wd_len; + + /** The ultimate return value for bfs_exec_finish(). */ + int ret; +}; + +/** + * Parse an exec action. + * + * @param argv + * The (bfs) command line argument to parse. + * @param flags + * Any flags for this exec action. + * @param ctx + * The bfs context. + * @return + * The parsed exec action, or NULL on failure. + */ +struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs_exec_flags flags); + +/** + * Execute the command for a file. + * + * @param execbuf + * The parsed exec action. + * @param ftwbuf + * The bftw() data for the current file. + * @return 0 if the command succeeded, -1 if it failed. If the command could + * be executed, -1 is returned, and errno will be non-zero. For + * BFS_EXEC_MULTI, errors will not be reported until bfs_exec_finish(). + */ +int bfs_exec(struct bfs_exec *execbuf, const struct BFTW *ftwbuf); + +/** + * Finish executing any commands. + * + * @param execbuf + * The parsed exec action. + * @return 0 on success, -1 if any errors were encountered. + */ +int bfs_exec_finish(struct bfs_exec *execbuf); + +/** + * Free a parsed exec action. + */ +void bfs_exec_free(struct bfs_exec *execbuf); + +#endif // BFS_EXEC_H diff --git a/src/expr.h b/src/expr.h new file mode 100644 index 0000000..1f1ece6 --- /dev/null +++ b/src/expr.h @@ -0,0 +1,235 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +/** + * The expression tree representation. + */ + +#ifndef BFS_EXPR_H +#define BFS_EXPR_H + +#include "color.h" +#include "eval.h" +#include "stat.h" +#include <stdbool.h> +#include <stddef.h> +#include <sys/types.h> +#include <time.h> + +/** + * Integer comparison modes. + */ +enum bfs_int_cmp { + /** Exactly N. */ + BFS_INT_EQUAL, + /** Less than N (-N). */ + BFS_INT_LESS, + /** Greater than N (+N). */ + BFS_INT_GREATER, +}; + +/** + * Permission comparison modes. + */ +enum bfs_mode_cmp { + /** Mode is an exact match (MODE). */ + BFS_MODE_EQUAL, + /** Mode has all these bits (-MODE). */ + BFS_MODE_ALL, + /** Mode has any of these bits (/MODE). */ + BFS_MODE_ANY, +}; + +/** + * Possible time units. + */ +enum bfs_time_unit { + /** Seconds. */ + BFS_SECONDS, + /** Minutes. */ + BFS_MINUTES, + /** Days. */ + BFS_DAYS, +}; + +/** + * Possible file size units. + */ +enum bfs_size_unit { + /** 512-byte blocks. */ + BFS_BLOCKS, + /** Single bytes. */ + BFS_BYTES, + /** Two-byte words. */ + BFS_WORDS, + /** Kibibytes. */ + BFS_KB, + /** Mebibytes. */ + BFS_MB, + /** Gibibytes. */ + BFS_GB, + /** Tebibytes. */ + BFS_TB, + /** Pebibytes. */ + BFS_PB, +}; + +/** + * A command line expression. + */ +struct bfs_expr { + /** The function that evaluates this expression. */ + bfs_eval_fn *eval_fn; + + /** The number of command line arguments for this expression. */ + size_t argc; + /** The command line arguments comprising this expression. */ + char **argv; + + /** The number of files this expression keeps open between evaluations. */ + int persistent_fds; + /** The number of files this expression opens during evaluation. */ + int ephemeral_fds; + + /** Whether this expression has no side effects. */ + bool pure; + /** Whether this expression always evaluates to true. */ + bool always_true; + /** Whether this expression always evaluates to false. */ + bool always_false; + /** Whether this expression doesn't appear on the command line. */ + bool synthetic; + + /** Estimated cost. */ + float cost; + /** Estimated probability of success. */ + float probability; + /** Number of times this predicate was evaluated. */ + size_t evaluations; + /** Number of times this predicate succeeded. */ + size_t successes; + /** Total time spent running this predicate. */ + struct timespec elapsed; + + /** Auxilliary data for the evaluation function. */ + union { + /** Child expressions. */ + struct { + /** The left hand side of the expression. */ + struct bfs_expr *lhs; + /** The right hand side of the expression. */ + struct bfs_expr *rhs; + }; + + /** Integer comparisons. */ + struct { + /** Integer for this comparison. */ + long long num; + /** The comparison mode. */ + enum bfs_int_cmp int_cmp; + + /** Optional extra data. */ + union { + /** -size data. */ + enum bfs_size_unit size_unit; + + /** Timestamp comparison data. */ + struct { + /** The stat field to look at. */ + enum bfs_stat_field stat_field; + /** The reference time. */ + struct timespec reftime; + /** The time unit. */ + enum bfs_time_unit time_unit; + }; + }; + }; + + /** Printing actions. */ + struct { + /** The output stream. */ + CFILE *cfile; + /** Optional -printf format. */ + struct bfs_printf *printf; + }; + + /** -exec data. */ + struct bfs_exec *exec; + + /** -flags data. */ + struct { + /** The comparison mode. */ + enum bfs_mode_cmp flags_cmp; + /** Flags that should be set. */ + unsigned long long set_flags; + /** Flags that should be cleared. */ + unsigned long long clear_flags; + }; + + /** -perm data. */ + struct { + /** The comparison mode. */ + enum bfs_mode_cmp mode_cmp; + /** Mode to use for files. */ + mode_t file_mode; + /** Mode to use for directories (different due to X). */ + mode_t dir_mode; + }; + + /** -regex data. */ + struct bfs_regex *regex; + + /** -samefile data. */ + struct { + /** Device number of the target file. */ + dev_t dev; + /** Inode number of the target file. */ + ino_t ino; + }; + }; +}; + +/** Singleton true expression instance. */ +extern struct bfs_expr bfs_true; +/** Singleton false expression instance. */ +extern struct bfs_expr bfs_false; + +/** + * Create a new expression. + */ +struct bfs_expr *bfs_expr_new(bfs_eval_fn *eval, size_t argc, char **argv); + +/** + * @return Whether the expression has child expressions. + */ +bool bfs_expr_has_children(const struct bfs_expr *expr); + +/** + * @return Whether expr is known to always quit. + */ +bool bfs_expr_never_returns(const struct bfs_expr *expr); + +/** + * @return The result of the integer comparison for this expression. + */ +bool bfs_expr_cmp(const struct bfs_expr *expr, long long n); + +/** + * Free an expression tree. + */ +void bfs_expr_free(struct bfs_expr *expr); + +#endif // BFS_EXPR_H diff --git a/src/fsade.c b/src/fsade.c new file mode 100644 index 0000000..1444cf4 --- /dev/null +++ b/src/fsade.c @@ -0,0 +1,392 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019-2021 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. * + ****************************************************************************/ + +#include "fsade.h" +#include "bftw.h" +#include "dir.h" +#include "dstring.h" +#include "util.h" +#include <errno.h> +#include <fcntl.h> +#include <stddef.h> +#include <unistd.h> + +#if BFS_CAN_CHECK_ACL +# include <sys/acl.h> +#endif + +#if BFS_CAN_CHECK_CAPABILITIES +# include <sys/capability.h> +#endif + +#if BFS_HAS_SYS_EXTATTR +# include <sys/extattr.h> +#elif BFS_HAS_SYS_XATTR +# include <sys/xattr.h> +#endif + +#if BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS + +/** + * Many of the APIs used here don't have *at() variants, but we can try to + * emulate something similar if /proc/self/fd is available. + */ +static const char *fake_at(const struct BFTW *ftwbuf) { + static bool proc_works = true; + static bool proc_checked = false; + + char *path = NULL; + if (!proc_works || ftwbuf->at_fd == AT_FDCWD) { + goto fail; + } + + path = dstrprintf("/proc/self/fd/%d/", ftwbuf->at_fd); + if (!path) { + goto fail; + } + + if (!proc_checked) { + proc_checked = true; + if (xfaccessat(AT_FDCWD, path, F_OK) != 0) { + proc_works = false; + goto fail; + } + } + + if (dstrcat(&path, ftwbuf->at_path) != 0) { + goto fail; + } + + return path; + +fail: + dstrfree(path); + return ftwbuf->path; +} + +static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { + if (path != ftwbuf->path) { + dstrfree((char *)path); + } +} + +/** + * Check if an error was caused by the absence of support or data for a feature. + */ +static bool is_absence_error(int error) { + // If the OS doesn't support the feature, it's obviously not enabled for + // any files + if (error == ENOTSUP) { + return true; + } + + // On Linux, ACLs and capabilities are implemented in terms of extended + // attributes, which report ENODATA/ENOATTR when missing + +#ifdef ENODATA + if (error == ENODATA) { + return true; + } +#endif + +#if defined(ENOATTR) && ENOATTR != ENODATA + if (error == ENOATTR) { + return true; + } +#endif + + // On at least FreeBSD and macOS, EINVAL is returned when the requested + // ACL type is not supported for that file + if (error == EINVAL) { + return true; + } + +#if __APPLE__ + // On macOS, ENOENT can also signal that a file has no ACLs + if (error == ENOENT) { + return true; + } +#endif + + return false; +} + +#endif // BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS + +#if BFS_CAN_CHECK_ACL + +/** Check if a POSIX.1e ACL is non-trivial. */ +static int bfs_check_posix1e_acl(acl_t acl, bool ignore_required) { + int ret = 0; + + acl_entry_t entry; + for (int status = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); +#if __APPLE__ + // POSIX.1e specifies a return value of 1 for success, but macOS + // returns 0 instead + status == 0; +#else + status > 0; +#endif + status = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { +#if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER) + if (ignore_required) { + acl_tag_t tag; + if (acl_get_tag_type(entry, &tag) != 0) { + ret = -1; + continue; + } + if (tag == ACL_USER_OBJ || tag == ACL_GROUP_OBJ || tag == ACL_OTHER) { + continue; + } + } +#endif + + ret = 1; + break; + } + + return ret; +} + +/** Check if an ACL of the given type is non-trivial. */ +static int bfs_check_acl_type(acl_t acl, acl_type_t type) { + if (type == ACL_TYPE_DEFAULT) { + // For directory default ACLs, any entries make them non-trivial + return bfs_check_posix1e_acl(acl, false); + } + +#if __FreeBSD__ + int trivial; + +#if BFS_HAS_FEATURE(memory_sanitizer, false) + // msan seems to be missing an interceptor for acl_is_trivial_np() + trivial = 0; +#endif + + if (acl_is_trivial_np(acl, &trivial) < 0) { + return -1; + } else if (trivial) { + return 0; + } else { + return 1; + } +#else // !__FreeBSD__ + return bfs_check_posix1e_acl(acl, true); +#endif +} + +int bfs_check_acl(const struct BFTW *ftwbuf) { + static const acl_type_t acl_types[] = { +#if __APPLE__ + // macOS gives EINVAL for either of the two standard ACL types, + // supporting only ACL_TYPE_EXTENDED + ACL_TYPE_EXTENDED, +#else + // The two standard POSIX.1e ACL types + ACL_TYPE_ACCESS, + ACL_TYPE_DEFAULT, +#endif + +#ifdef ACL_TYPE_NFS4 + ACL_TYPE_NFS4, +#endif + }; + static const size_t n_acl_types = sizeof(acl_types)/sizeof(acl_types[0]); + + if (ftwbuf->type == BFS_LNK) { + return 0; + } + + const char *path = fake_at(ftwbuf); + + int ret = -1, error = 0; + for (size_t i = 0; i < n_acl_types && ret <= 0; ++i) { + acl_type_t type = acl_types[i]; + + if (type == ACL_TYPE_DEFAULT && ftwbuf->type != BFS_DIR) { + // ACL_TYPE_DEFAULT is supported only for directories, + // otherwise acl_get_file() gives EACCESS + continue; + } + + acl_t acl = acl_get_file(path, type); + if (!acl) { + error = errno; + if (is_absence_error(error)) { + ret = 0; + } + continue; + } + + ret = bfs_check_acl_type(acl, type); + error = errno; + acl_free(acl); + } + + free_fake_at(ftwbuf, path); + errno = error; + return ret; +} + +#else // !BFS_CAN_CHECK_ACL + +int bfs_check_acl(const struct BFTW *ftwbuf) { + errno = ENOTSUP; + return -1; +} + +#endif + +#if BFS_CAN_CHECK_CAPABILITIES + +int bfs_check_capabilities(const struct BFTW *ftwbuf) { + if (ftwbuf->type == BFS_LNK) { + return 0; + } + + int ret = -1, error; + const char *path = fake_at(ftwbuf); + + cap_t caps = cap_get_file(path); + if (!caps) { + error = errno; + if (is_absence_error(error)) { + ret = 0; + } + goto out_path; + } + + // TODO: Any better way to check for a non-empty capability set? + char *text = cap_to_text(caps, NULL); + if (!text) { + error = errno; + goto out_caps; + } + ret = text[0] ? 1 : 0; + + error = errno; + cap_free(text); +out_caps: + cap_free(caps); +out_path: + free_fake_at(ftwbuf, path); + errno = error; + return ret; +} + +#else // !BFS_CAN_CHECK_CAPABILITIES + +int bfs_check_capabilities(const struct BFTW *ftwbuf) { + errno = ENOTSUP; + return -1; +} + +#endif + +#if BFS_CAN_CHECK_XATTRS + +int bfs_check_xattrs(const struct BFTW *ftwbuf) { + const char *path = fake_at(ftwbuf); + ssize_t len; + +#if BFS_HAS_SYS_EXTATTR + ssize_t (*extattr_list)(const char *, int, void*, size_t) = + ftwbuf->type == BFS_LNK ? extattr_list_link : extattr_list_file; + + len = extattr_list(path, EXTATTR_NAMESPACE_SYSTEM, NULL, 0); + if (len <= 0) { + len = extattr_list(path, EXTATTR_NAMESPACE_USER, NULL, 0); + } +#elif __APPLE__ + int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0; + len = listxattr(path, NULL, 0, options); +#else + if (ftwbuf->type == BFS_LNK) { + len = llistxattr(path, NULL, 0); + } else { + len = listxattr(path, NULL, 0); + } +#endif + + int error = errno; + + free_fake_at(ftwbuf, path); + + if (len > 0) { + return 1; + } else if (len == 0 || is_absence_error(error)) { + return 0; + } else if (error == E2BIG) { + return 1; + } else { + errno = error; + return -1; + } +} + +int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { + const char *path = fake_at(ftwbuf); + ssize_t len; + +#if BFS_HAS_SYS_EXTATTR + ssize_t (*extattr_get)(const char *, int, const char *, void*, size_t) = + ftwbuf->type == BFS_LNK ? extattr_get_link : extattr_get_file; + + len = extattr_get(path, EXTATTR_NAMESPACE_SYSTEM, name, NULL, 0); + if (len < 0) { + len = extattr_get(path, EXTATTR_NAMESPACE_USER, name, NULL, 0); + } +#elif __APPLE__ + int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0; + len = getxattr(path, name, NULL, 0, 0, options); +#else + if (ftwbuf->type == BFS_LNK) { + len = lgetxattr(path, name, NULL, 0); + } else { + len = getxattr(path, name, NULL, 0); + } +#endif + + int error = errno; + + free_fake_at(ftwbuf, path); + + if (len >= 0) { + return 1; + } else if (is_absence_error(error)) { + return 0; + } else if (error == E2BIG) { + return 1; + } else { + errno = error; + return -1; + } +} + +#else // !BFS_CAN_CHECK_XATTRS + +int bfs_check_xattrs(const struct BFTW *ftwbuf) { + errno = ENOTSUP; + return -1; +} + +int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { + errno = ENOTSUP; + return -1; +} + +#endif diff --git a/src/fsade.h b/src/fsade.h new file mode 100644 index 0000000..e964112 --- /dev/null +++ b/src/fsade.h @@ -0,0 +1,83 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019-2020 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. * + ****************************************************************************/ + +/** + * A facade over (file)system features that are (un)implemented differently + * between platforms. + */ + +#ifndef BFS_FSADE_H +#define BFS_FSADE_H + +#include "util.h" +#include <stdbool.h> + +#define BFS_CAN_CHECK_ACL BFS_HAS_SYS_ACL + +#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_HAS_SYS_CAPABILITY && !__FreeBSD__ +# include <sys/capability.h> +# ifdef CAP_CHOWN +# define BFS_CAN_CHECK_CAPABILITIES true +# endif +#endif + +#define BFS_CAN_CHECK_XATTRS (BFS_HAS_SYS_EXTATTR || BFS_HAS_SYS_XATTR) + +struct BFTW; + +/** + * Check if a file has a non-trivial Access Control List. + * + * @param ftwbuf + * The file to check. + * @return + * 1 if it does, 0 if it doesn't, or -1 if an error occurred. + */ +int bfs_check_acl(const struct BFTW *ftwbuf); + +/** + * Check if a file has a non-trivial capability set. + * + * @param ftwbuf + * The file to check. + * @return + * 1 if it does, 0 if it doesn't, or -1 if an error occurred. + */ +int bfs_check_capabilities(const struct BFTW *ftwbuf); + +/** + * Check if a file has any extended attributes set. + * + * @param ftwbuf + * The file to check. + * @return + * 1 if it does, 0 if it doesn't, or -1 if an error occurred. + */ +int bfs_check_xattrs(const struct BFTW *ftwbuf); + +/** + * Check if a file has an extended attribute with the given name. + * + * @param ftwbuf + * The file to check. + * @param name + * The name of the xattr to check. + * @return + * 1 if it does, 0 if it doesn't, or -1 if an error occurred. + */ +int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name); + +#endif // BFS_FSADE_H diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..9dc96e4 --- /dev/null +++ b/src/main.c @@ -0,0 +1,141 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +/** + * - main(): the entry point for bfs(1), a breadth-first version of find(1) + * - main.c (this file) + * + * - bfs_parse_cmdline(): parses the command line into an expression tree + * - ctx.[ch] (struct bfs_ctx, the overall bfs context) + * - expr.h (declares the expression tree nodes) + * - parse.[ch] (the parser itself) + * - opt.[ch] (the optimizer) + * + * - bfs_eval(): runs the expression on every file it sees + * - eval.[ch] (the main evaluation functions) + * - exec.[ch] (implements -exec[dir]/-ok[dir]) + * - printf.[ch] (implements -[f]printf) + * + * - bftw(): used by bfs_eval() to walk the directory tree(s) + * - bftw.[ch] (an extended version of nftw(3)) + * + * - Utilities: + * - bfs.h (constants about bfs itself) + * - bar.[ch] (a terminal status bar) + * - color.[ch] (for pretty terminal colors) + * - darray.[ch] (a dynamic array library) + * - diag.[ch] (formats diagnostic messages) + * - dir.[ch] (a directory API facade) + * - dstring.[ch] (a dynamic string library) + * - fsade.[ch] (a facade over non-standard filesystem features) + * - mtab.[ch] (parses the system's mount table) + * - pwcache.[ch] (a cache for the user/group tables) + * - stat.[ch] (wraps stat(), or statx() on Linux) + * - trie.[ch] (a trie set/map implementation) + * - typo.[ch] (fuzzy matching for typos) + * - util.[ch] (everything else) + * - xregex.[ch] (regular expression support) + * - xspawn.[ch] (spawns processes) + * - xtime.[ch] (date/time handling utilities) + */ + +#include "ctx.h" +#include "eval.h" +#include "parse.h" +#include "util.h" +#include <errno.h> +#include <fcntl.h> +#include <locale.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +/** + * Check if a file descriptor is open. + */ +static bool isopen(int fd) { + return fcntl(fd, F_GETFD) >= 0 || errno != EBADF; +} + +/** + * Open a file and redirect it to a particular descriptor. + */ +static int redirect(int fd, const char *path, int flags) { + int newfd = open(path, flags); + if (newfd < 0 || newfd == fd) { + return newfd; + } + + int ret = dup2(newfd, fd); + close_quietly(newfd); + return ret; +} + +/** + * Make sure the standard streams std{in,out,err} are open. If they are not, + * future open() calls may use those file descriptors, and std{in,out,err} will + * use them unintentionally. + */ +static int open_std_streams(void) { +#ifdef O_PATH + const int inflags = O_PATH, outflags = O_PATH; +#else + // These are intentionally backwards so that bfs >&- still fails with EBADF + const int inflags = O_WRONLY, outflags = O_RDONLY; +#endif + + if (!isopen(STDERR_FILENO) && redirect(STDERR_FILENO, "/dev/null", outflags) < 0) { + return -1; + } + if (!isopen(STDOUT_FILENO) && redirect(STDOUT_FILENO, "/dev/null", outflags) < 0) { + perror("redirect()"); + return -1; + } + if (!isopen(STDIN_FILENO) && redirect(STDIN_FILENO, "/dev/null", inflags) < 0) { + perror("redirect()"); + return -1; + } + + return 0; +} + +/** + * bfs entry point. + */ +int main(int argc, char *argv[]) { + int ret = EXIT_FAILURE; + + // Make sure the standard streams are open + if (open_std_streams() != 0) { + goto done; + } + + // Use the system locale instead of "C" + setlocale(LC_ALL, ""); + + struct bfs_ctx *ctx = bfs_parse_cmdline(argc, argv); + if (ctx) { + ret = bfs_eval(ctx); + } + + if (bfs_ctx_free(ctx) != 0 && ret == EXIT_SUCCESS) { + ret = EXIT_FAILURE; + } + +done: + return ret; +} diff --git a/src/mtab.c b/src/mtab.c new file mode 100644 index 0000000..adc3f58 --- /dev/null +++ b/src/mtab.c @@ -0,0 +1,246 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-2020 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. * + ****************************************************************************/ + +#include "mtab.h" +#include "darray.h" +#include "stat.h" +#include "trie.h" +#include "util.h" +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> + +#if BFS_HAS_SYS_PARAM +# include <sys/param.h> +#endif + +#if BFS_HAS_MNTENT +# define BFS_MNTENT 1 +#elif BSD +# define BFS_MNTINFO 1 +#elif __SVR4 +# define BFS_MNTTAB 1 +#endif + +#if BFS_MNTENT +# include <mntent.h> +# include <paths.h> +# include <stdio.h> +#elif BFS_MNTINFO +# include <sys/mount.h> +# include <sys/ucred.h> +#elif BFS_MNTTAB +# include <stdio.h> +# include <sys/mnttab.h> +#endif + +/** + * A mount point in the table. + */ +struct bfs_mtab_entry { + /** The path to the mount point. */ + char *path; + /** The filesystem type. */ + char *type; +}; + +struct bfs_mtab { + /** The list of mount points. */ + struct bfs_mtab_entry *entries; + /** The basenames of every mount point. */ + struct trie names; + + /** A map from device ID to fstype (populated lazily). */ + struct trie types; + /** Whether the types map has been populated. */ + bool types_filled; +}; + +/** + * Add an entry to the mount table. + */ +static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) { + struct bfs_mtab_entry entry = { + .path = strdup(path), + .type = strdup(type), + }; + + if (!entry.path || !entry.type) { + goto fail_entry; + } + + if (DARRAY_PUSH(&mtab->entries, &entry) != 0) { + goto fail_entry; + } + + if (!trie_insert_str(&mtab->names, xbasename(path))) { + goto fail; + } + + return 0; + +fail_entry: + free(entry.type); + free(entry.path); +fail: + return -1; +} + +struct bfs_mtab *bfs_mtab_parse(void) { + struct bfs_mtab *mtab = malloc(sizeof(*mtab)); + if (!mtab) { + return NULL; + } + + mtab->entries = NULL; + trie_init(&mtab->names); + trie_init(&mtab->types); + mtab->types_filled = false; + + int error = 0; + +#if BFS_MNTENT + + FILE *file = setmntent(_PATH_MOUNTED, "r"); + if (!file) { + // In case we're in a chroot or something with /proc but no /etc/mtab + error = errno; + file = setmntent("/proc/mounts", "r"); + } + if (!file) { + goto fail; + } + + struct mntent *mnt; + while ((mnt = getmntent(file))) { + if (bfs_mtab_add(mtab, mnt->mnt_dir, mnt->mnt_type) != 0) { + error = errno; + endmntent(file); + goto fail; + } + } + + endmntent(file); + +#elif BFS_MNTINFO + +#if __NetBSD__ + typedef struct statvfs bfs_statfs; +#else + typedef struct statfs bfs_statfs; +#endif + + bfs_statfs *mntbuf; + int size = getmntinfo(&mntbuf, MNT_WAIT); + if (size < 0) { + error = errno; + goto fail; + } + + for (bfs_statfs *mnt = mntbuf; mnt < mntbuf + size; ++mnt) { + if (bfs_mtab_add(mtab, mnt->f_mntonname, mnt->f_fstypename) != 0) { + error = errno; + goto fail; + } + } + +#elif BFS_MNTTAB + + FILE *file = xfopen(MNTTAB, O_RDONLY | O_CLOEXEC); + if (!file) { + error = errno; + goto fail; + } + + struct mnttab mnt; + while (getmntent(file, &mnt) == 0) { + if (bfs_mtab_add(mtab, mnt.mnt_mountp, mnt.mnt_fstype) != 0) { + error = errno; + fclose(file); + goto fail; + } + } + + fclose(file); + +#else + + error = ENOTSUP; + goto fail; + +#endif + + return mtab; + +fail: + bfs_mtab_free(mtab); + errno = error; + return NULL; +} + +static void bfs_mtab_fill_types(struct bfs_mtab *mtab) { + for (size_t i = 0; i < darray_length(mtab->entries); ++i) { + struct bfs_mtab_entry *entry = &mtab->entries[i]; + + struct bfs_stat sb; + if (bfs_stat(AT_FDCWD, entry->path, BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC, &sb) != 0) { + continue; + } + + struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev)); + if (leaf) { + leaf->value = entry->type; + } + } + + mtab->types_filled = true; +} + +const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) { + if (!mtab->types_filled) { + bfs_mtab_fill_types((struct bfs_mtab *)mtab); + } + + const struct trie_leaf *leaf = trie_find_mem(&mtab->types, &statbuf->dev, sizeof(statbuf->dev)); + if (leaf) { + return leaf->value; + } else { + return "unknown"; + } +} + +bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path) { + const char *name = xbasename(path); + return trie_find_str(&mtab->names, name); +} + +void bfs_mtab_free(struct bfs_mtab *mtab) { + if (mtab) { + trie_destroy(&mtab->types); + trie_destroy(&mtab->names); + + for (size_t i = 0; i < darray_length(mtab->entries); ++i) { + free(mtab->entries[i].type); + free(mtab->entries[i].path); + } + darray_free(mtab->entries); + + free(mtab); + } +} diff --git a/src/mtab.h b/src/mtab.h new file mode 100644 index 0000000..807539d --- /dev/null +++ b/src/mtab.h @@ -0,0 +1,71 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-2020 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. * + ****************************************************************************/ + +/** + * A facade over platform-specific APIs for enumerating mounted filesystems. + */ + +#ifndef BFS_MTAB_H +#define BFS_MTAB_H + +#include <stdbool.h> + +struct bfs_stat; + +/** + * A file system mount table. + */ +struct bfs_mtab; + +/** + * Parse the mount table. + * + * @return + * The parsed mount table, or NULL on error. + */ +struct bfs_mtab *bfs_mtab_parse(void); + +/** + * Determine the file system type that a file is on. + * + * @param mtab + * The current mount table. + * @param statbuf + * The bfs_stat() buffer for the file in question. + * @return + * The type of file system containing this file, "unknown" if not known, + * or NULL on error. + */ +const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf); + +/** + * Check if a file could be a mount point. + * + * @param mtab + * The current mount table. + * @param path + * The path to check. + * @return + * Whether the named file could be a mount point. + */ +bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path); + +/** + * Free a mount table. + */ +void bfs_mtab_free(struct bfs_mtab *mtab); + +#endif // BFS_MTAB_H diff --git a/src/opt.c b/src/opt.c new file mode 100644 index 0000000..f8c0ba3 --- /dev/null +++ b/src/opt.c @@ -0,0 +1,1088 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-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. * + ****************************************************************************/ + +/** + * The expression optimizer. Different optimization levels are supported: + * + * -O1: basic logical simplifications, like folding (-true -and -foo) to -foo. + * + * -O2: dead code elimination and data flow analysis. struct opt_facts is used + * to record data flow facts that are true at various points of evaluation. + * Specifically, struct opt_facts records the facts that must be true before an + * expression is evaluated (state->facts), and those that must be true after the + * expression is evaluated, given that it returns true (state->facts_when_true) + * or false (state->facts_when_true). Additionally, state->facts_when_impure + * records the possible data flow facts before any expressions with side effects + * are evaluated. + * + * -O3: expression re-ordering to reduce expected cost. In an expression like + * (-foo -and -bar), if both -foo and -bar are pure (no side effects), they can + * be re-ordered to (-bar -and -foo). This is profitable if the expected cost + * is lower for the re-ordered expression, for example if -foo is very slow or + * -bar is likely to return false. + * + * -O4/-Ofast: aggressive optimizations that may affect correctness in corner + * cases. The main effect is to use facts_when_impure to determine if any side- + * effects are reachable at all, and skipping the traversal if not. + */ + +#include "opt.h" +#include "color.h" +#include "ctx.h" +#include "diag.h" +#include "eval.h" +#include "expr.h" +#include "pwcache.h" +#include "util.h" +#include <assert.h> +#include <limits.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +static char *fake_and_arg = "-a"; +static char *fake_or_arg = "-o"; +static char *fake_not_arg = "!"; + +/** + * A contrained integer range. + */ +struct range { + /** The (inclusive) minimum value. */ + long long min; + /** The (inclusive) maximum value. */ + long long max; +}; + +/** Compute the minimum of two values. */ +static long long min_value(long long a, long long b) { + if (a < b) { + return a; + } else { + return b; + } +} + +/** Compute the maximum of two values. */ +static long long max_value(long long a, long long b) { + if (a > b) { + return a; + } else { + return b; + } +} + +/** Constrain the minimum of a range. */ +static void constrain_min(struct range *range, long long value) { + range->min = max_value(range->min, value); +} + +/** Contrain the maximum of a range. */ +static void constrain_max(struct range *range, long long value) { + range->max = min_value(range->max, value); +} + +/** Remove a single value from a range. */ +static void range_remove(struct range *range, long long value) { + if (range->min == value) { + if (range->min == LLONG_MAX) { + range->max = LLONG_MIN; + } else { + ++range->min; + } + } + + if (range->max == value) { + if (range->max == LLONG_MIN) { + range->min = LLONG_MAX; + } else { + --range->max; + } + } +} + +/** Compute the union of two ranges. */ +static void range_union(struct range *result, const struct range *lhs, const struct range *rhs) { + result->min = min_value(lhs->min, rhs->min); + result->max = max_value(lhs->max, rhs->max); +} + +/** Check if a range contains no values. */ +static bool range_is_impossible(const struct range *range) { + return range->min > range->max; +} + +/** Set a range to contain no values. */ +static void set_range_impossible(struct range *range) { + range->min = LLONG_MAX; + range->max = LLONG_MIN; +} + +/** + * Types of ranges we track. + */ +enum range_type { + /** Search tree depth. */ + DEPTH_RANGE, + /** Group ID. */ + GID_RANGE, + /** Inode number. */ + INUM_RANGE, + /** Hard link count. */ + LINKS_RANGE, + /** File size. */ + SIZE_RANGE, + /** User ID. */ + UID_RANGE, + /** The number of range_types. */ + RANGE_TYPES, +}; + +/** + * A possibly-known value of a predicate. + */ +enum known_pred { + /** The state is impossible to reach. */ + PRED_IMPOSSIBLE = -2, + /** The value of the predicate is not known. */ + PRED_UNKNOWN = -1, + /** The predicate is known to be false. */ + PRED_FALSE = false, + /** The predicate is known to be true. */ + PRED_TRUE = true, +}; + +/** Make a predicate known. */ +static void constrain_pred(enum known_pred *pred, bool value) { + if (*pred == PRED_UNKNOWN) { + *pred = value; + } else if (*pred == !value) { + *pred = PRED_IMPOSSIBLE; + } +} + +/** Compute the union of two known predicates. */ +static enum known_pred pred_union(enum known_pred lhs, enum known_pred rhs) { + if (lhs == PRED_IMPOSSIBLE) { + return rhs; + } else if (rhs == PRED_IMPOSSIBLE) { + return lhs; + } else if (lhs == rhs) { + return lhs; + } else { + return PRED_UNKNOWN; + } +} + +/** + * Types of predicates we track. + */ +enum pred_type { + /** -readable */ + READABLE_PRED, + /** -writable */ + WRITABLE_PRED, + /** -executable */ + EXECUTABLE_PRED, + /** -acl */ + ACL_PRED, + /** -capable */ + CAPABLE_PRED, + /** -empty */ + EMPTY_PRED, + /** -hidden */ + HIDDEN_PRED, + /** -nogroup */ + NOGROUP_PRED, + /** -nouser */ + NOUSER_PRED, + /** -sparse */ + SPARSE_PRED, + /** -xattr */ + XATTR_PRED, + /** The number of pred_types. */ + PRED_TYPES, +}; + +/** + * Data flow facts about an evaluation point. + */ +struct opt_facts { + /** The value ranges we track. */ + struct range ranges[RANGE_TYPES]; + + /** The predicates we track. */ + enum known_pred preds[PRED_TYPES]; + + /** Bitmask of possible file types. */ + unsigned int types; + /** Bitmask of possible link target types. */ + unsigned int xtypes; +}; + +/** Initialize some data flow facts. */ +static void facts_init(struct opt_facts *facts) { + for (int i = 0; i < RANGE_TYPES; ++i) { + struct range *range = &facts->ranges[i]; + range->min = 0; // All ranges we currently track are non-negative + range->max = LLONG_MAX; + } + + for (int i = 0; i < PRED_TYPES; ++i) { + facts->preds[i] = PRED_UNKNOWN; + } + + facts->types = ~0; + facts->xtypes = ~0; +} + +/** Compute the union of two fact sets. */ +static void facts_union(struct opt_facts *result, const struct opt_facts *lhs, const struct opt_facts *rhs) { + for (int i = 0; i < RANGE_TYPES; ++i) { + range_union(&result->ranges[i], &lhs->ranges[i], &rhs->ranges[i]); + } + + for (int i = 0; i < PRED_TYPES; ++i) { + result->preds[i] = pred_union(lhs->preds[i], rhs->preds[i]); + } + + result->types = lhs->types | rhs->types; + result->xtypes = lhs->xtypes | rhs->xtypes; +} + +/** Determine whether a fact set is impossible. */ +static bool facts_are_impossible(const struct opt_facts *facts) { + for (int i = 0; i < RANGE_TYPES; ++i) { + if (range_is_impossible(&facts->ranges[i])) { + return true; + } + } + + for (int i = 0; i < PRED_TYPES; ++i) { + if (facts->preds[i] == PRED_IMPOSSIBLE) { + return true; + } + } + + if (!facts->types || !facts->xtypes) { + return true; + } + + return false; +} + +/** Set some facts to be impossible. */ +static void set_facts_impossible(struct opt_facts *facts) { + for (int i = 0; i < RANGE_TYPES; ++i) { + set_range_impossible(&facts->ranges[i]); + } + + for (int i = 0; i < PRED_TYPES; ++i) { + facts->preds[i] = PRED_IMPOSSIBLE; + } + + facts->types = 0; + facts->xtypes = 0; +} + +/** + * Optimizer state. + */ +struct opt_state { + /** The context we're optimizing. */ + const struct bfs_ctx *ctx; + + /** Data flow facts before this expression is evaluated. */ + struct opt_facts facts; + /** Data flow facts after this expression returns true. */ + struct opt_facts facts_when_true; + /** Data flow facts after this expression returns false. */ + struct opt_facts facts_when_false; + /** Data flow facts before any side-effecting expressions are evaluated. */ + struct opt_facts *facts_when_impure; +}; + +/** Log an optimization. */ +BFS_FORMATTER(3, 4) +static bool opt_debug(const struct opt_state *state, int level, const char *format, ...) { + assert(state->ctx->optlevel >= level); + + if (bfs_debug(state->ctx, DEBUG_OPT, "${cyn}-O%d${rs}: ", level)) { + va_list args; + va_start(args, format); + cvfprintf(state->ctx->cerr, format, args); + va_end(args); + return true; + } else { + return false; + } +} + +/** Warn about an expression. */ +BFS_FORMATTER(3, 4) +static void opt_warning(const struct opt_state *state, const struct bfs_expr *expr, const char *format, ...) { + if (bfs_expr_warning(state->ctx, expr)) { + va_list args; + va_start(args, format); + bfs_warning(state->ctx, format, args); + va_end(args); + } +} + +/** Extract a child expression, freeing the outer expression. */ +static struct bfs_expr *extract_child_expr(struct bfs_expr *expr, struct bfs_expr **child) { + struct bfs_expr *ret = *child; + *child = NULL; + bfs_expr_free(expr); + return ret; +} + +/** + * Negate an expression. + */ +static struct bfs_expr *negate_expr(struct bfs_expr *rhs, char **argv) { + if (rhs->eval_fn == eval_not) { + return extract_child_expr(rhs, &rhs->rhs); + } + + struct bfs_expr *expr = bfs_expr_new(eval_not, 1, argv); + if (!expr) { + bfs_expr_free(rhs); + return NULL; + } + + if (argv == &fake_not_arg) { + expr->synthetic = true; + } + + expr->lhs = NULL; + expr->rhs = rhs; + return expr; +} + +static struct bfs_expr *optimize_not_expr(const struct opt_state *state, struct bfs_expr *expr); +static struct bfs_expr *optimize_and_expr(const struct opt_state *state, struct bfs_expr *expr); +static struct bfs_expr *optimize_or_expr(const struct opt_state *state, struct bfs_expr *expr); + +/** + * Apply De Morgan's laws. + */ +static struct bfs_expr *de_morgan(const struct opt_state *state, struct bfs_expr *expr, char **argv) { + bool debug = opt_debug(state, 1, "De Morgan's laws: %pe ", expr); + + struct bfs_expr *parent = negate_expr(expr, argv); + if (!parent) { + return NULL; + } + + bool has_parent = true; + if (parent->eval_fn != eval_not) { + expr = parent; + has_parent = false; + } + + assert(expr->eval_fn == eval_and || expr->eval_fn == eval_or); + if (expr->eval_fn == eval_and) { + expr->eval_fn = eval_or; + expr->argv = &fake_or_arg; + } else { + expr->eval_fn = eval_and; + expr->argv = &fake_and_arg; + } + expr->synthetic = true; + + expr->lhs = negate_expr(expr->lhs, argv); + expr->rhs = negate_expr(expr->rhs, argv); + if (!expr->lhs || !expr->rhs) { + bfs_expr_free(parent); + return NULL; + } + + if (debug) { + cfprintf(state->ctx->cerr, "<==> %pe\n", parent); + } + + if (expr->lhs->eval_fn == eval_not) { + expr->lhs = optimize_not_expr(state, expr->lhs); + } + if (expr->rhs->eval_fn == eval_not) { + expr->rhs = optimize_not_expr(state, expr->rhs); + } + if (!expr->lhs || !expr->rhs) { + bfs_expr_free(parent); + return NULL; + } + + if (expr->eval_fn == eval_and) { + expr = optimize_and_expr(state, expr); + } else { + expr = optimize_or_expr(state, expr); + } + if (has_parent) { + parent->rhs = expr; + } else { + parent = expr; + } + if (!expr) { + bfs_expr_free(parent); + return NULL; + } + + if (has_parent) { + parent = optimize_not_expr(state, parent); + } + return parent; +} + +/** Optimize an expression recursively. */ +static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr); + +/** + * Optimize a negation. + */ +static struct bfs_expr *optimize_not_expr(const struct opt_state *state, struct bfs_expr *expr) { + assert(expr->eval_fn == eval_not); + + struct bfs_expr *rhs = expr->rhs; + + int optlevel = state->ctx->optlevel; + if (optlevel >= 1) { + if (rhs == &bfs_true) { + opt_debug(state, 1, "constant propagation: %pe <==> %pe\n", expr, &bfs_false); + bfs_expr_free(expr); + return &bfs_false; + } else if (rhs == &bfs_false) { + opt_debug(state, 1, "constant propagation: %pe <==> %pe\n", expr, &bfs_true); + bfs_expr_free(expr); + return &bfs_true; + } else if (rhs->eval_fn == eval_not) { + opt_debug(state, 1, "double negation: %pe <==> %pe\n", expr, rhs->rhs); + return extract_child_expr(expr, &rhs->rhs); + } else if (bfs_expr_never_returns(rhs)) { + opt_debug(state, 1, "reachability: %pe <==> %pe\n", expr, rhs); + return extract_child_expr(expr, &expr->rhs); + } else if ((rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) + && (rhs->lhs->eval_fn == eval_not || rhs->rhs->eval_fn == eval_not)) { + return de_morgan(state, expr, expr->argv); + } + } + + expr->pure = rhs->pure; + expr->always_true = rhs->always_false; + expr->always_false = rhs->always_true; + expr->cost = rhs->cost; + expr->probability = 1.0 - rhs->probability; + + return expr; +} + +/** Optimize a negation recursively. */ +static struct bfs_expr *optimize_not_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { + struct opt_state rhs_state = *state; + expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); + if (!expr->rhs) { + goto fail; + } + + state->facts_when_true = rhs_state.facts_when_false; + state->facts_when_false = rhs_state.facts_when_true; + + return optimize_not_expr(state, expr); + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** Optimize a conjunction. */ +static struct bfs_expr *optimize_and_expr(const struct opt_state *state, struct bfs_expr *expr) { + assert(expr->eval_fn == eval_and); + + struct bfs_expr *lhs = expr->lhs; + struct bfs_expr *rhs = expr->rhs; + + const struct bfs_ctx *ctx = state->ctx; + int optlevel = ctx->optlevel; + if (optlevel >= 1) { + if (lhs == &bfs_true) { + opt_debug(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, rhs); + return extract_child_expr(expr, &expr->rhs); + } else if (rhs == &bfs_true) { + opt_debug(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, lhs); + return extract_child_expr(expr, &expr->lhs); + } else if (lhs->always_false) { + opt_debug(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs); + opt_warning(state, expr->rhs, "This expression is unreachable.\n\n"); + return extract_child_expr(expr, &expr->lhs); + } else if (lhs->always_true && rhs == &bfs_false) { + bool debug = opt_debug(state, 1, "strength reduction: %pe <==> ", expr); + struct bfs_expr *ret = extract_child_expr(expr, &expr->lhs); + ret = negate_expr(ret, &fake_not_arg); + if (debug && ret) { + cfprintf(ctx->cerr, "%pe\n", ret); + } + return ret; + } else if (optlevel >= 2 && lhs->pure && rhs == &bfs_false) { + opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs); + opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n"); + return extract_child_expr(expr, &expr->rhs); + } else if (lhs->eval_fn == eval_not && rhs->eval_fn == eval_not) { + return de_morgan(state, expr, expr->lhs->argv); + } + } + + expr->pure = lhs->pure && rhs->pure; + expr->always_true = lhs->always_true && rhs->always_true; + expr->always_false = lhs->always_false || rhs->always_false; + expr->cost = lhs->cost + lhs->probability*rhs->cost; + expr->probability = lhs->probability*rhs->probability; + + return expr; +} + +/** Optimize a conjunction recursively. */ +static struct bfs_expr *optimize_and_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { + struct opt_state lhs_state = *state; + expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs); + if (!expr->lhs) { + goto fail; + } + + struct opt_state rhs_state = *state; + rhs_state.facts = lhs_state.facts_when_true; + expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); + if (!expr->rhs) { + goto fail; + } + + state->facts_when_true = rhs_state.facts_when_true; + facts_union(&state->facts_when_false, &lhs_state.facts_when_false, &rhs_state.facts_when_false); + + return optimize_and_expr(state, expr); + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** Optimize a disjunction. */ +static struct bfs_expr *optimize_or_expr(const struct opt_state *state, struct bfs_expr *expr) { + assert(expr->eval_fn == eval_or); + + struct bfs_expr *lhs = expr->lhs; + struct bfs_expr *rhs = expr->rhs; + + const struct bfs_ctx *ctx = state->ctx; + int optlevel = ctx->optlevel; + if (optlevel >= 1) { + if (lhs->always_true) { + opt_debug(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs); + opt_warning(state, expr->rhs, "This expression is unreachable.\n\n"); + return extract_child_expr(expr, &expr->lhs); + } else if (lhs == &bfs_false) { + opt_debug(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, rhs); + return extract_child_expr(expr, &expr->rhs); + } else if (rhs == &bfs_false) { + opt_debug(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, lhs); + return extract_child_expr(expr, &expr->lhs); + } else if (lhs->always_false && rhs == &bfs_true) { + bool debug = opt_debug(state, 1, "strength reduction: %pe <==> ", expr); + struct bfs_expr *ret = extract_child_expr(expr, &expr->lhs); + ret = negate_expr(ret, &fake_not_arg); + if (debug && ret) { + cfprintf(ctx->cerr, "%pe\n", ret); + } + return ret; + } else if (optlevel >= 2 && lhs->pure && rhs == &bfs_true) { + opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs); + opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n"); + return extract_child_expr(expr, &expr->rhs); + } else if (lhs->eval_fn == eval_not && rhs->eval_fn == eval_not) { + return de_morgan(state, expr, expr->lhs->argv); + } + } + + expr->pure = lhs->pure && rhs->pure; + expr->always_true = lhs->always_true || rhs->always_true; + expr->always_false = lhs->always_false && rhs->always_false; + expr->cost = lhs->cost + (1 - lhs->probability)*rhs->cost; + expr->probability = lhs->probability + rhs->probability - lhs->probability*rhs->probability; + + return expr; +} + +/** Optimize a disjunction recursively. */ +static struct bfs_expr *optimize_or_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { + struct opt_state lhs_state = *state; + expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs); + if (!expr->lhs) { + goto fail; + } + + struct opt_state rhs_state = *state; + rhs_state.facts = lhs_state.facts_when_false; + expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); + if (!expr->rhs) { + goto fail; + } + + facts_union(&state->facts_when_true, &lhs_state.facts_when_true, &rhs_state.facts_when_true); + state->facts_when_false = rhs_state.facts_when_false; + + return optimize_or_expr(state, expr); + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** Optimize an expression in an ignored-result context. */ +static struct bfs_expr *ignore_result(const struct opt_state *state, struct bfs_expr *expr) { + int optlevel = state->ctx->optlevel; + + if (optlevel >= 1) { + while (true) { + if (expr->eval_fn == eval_not) { + opt_debug(state, 1, "ignored result: %pe --> %pe\n", expr, expr->rhs); + opt_warning(state, expr, "The result of this expression is ignored.\n\n"); + expr = extract_child_expr(expr, &expr->rhs); + } else if (optlevel >= 2 + && (expr->eval_fn == eval_and || expr->eval_fn == eval_or || expr->eval_fn == eval_comma) + && expr->rhs->pure) { + opt_debug(state, 2, "ignored result: %pe --> %pe\n", expr, expr->lhs); + opt_warning(state, expr->rhs, "The result of this expression is ignored.\n\n"); + expr = extract_child_expr(expr, &expr->lhs); + } else { + break; + } + } + + if (optlevel >= 2 && expr->pure && expr != &bfs_false) { + opt_debug(state, 2, "ignored result: %pe --> %pe\n", expr, &bfs_false); + opt_warning(state, expr, "The result of this expression is ignored.\n\n"); + bfs_expr_free(expr); + expr = &bfs_false; + } + } + + return expr; +} + +/** Optimize a comma expression. */ +static struct bfs_expr *optimize_comma_expr(const struct opt_state *state, struct bfs_expr *expr) { + assert(expr->eval_fn == eval_comma); + + struct bfs_expr *lhs = expr->lhs; + struct bfs_expr *rhs = expr->rhs; + + int optlevel = state->ctx->optlevel; + if (optlevel >= 1) { + lhs = expr->lhs = ignore_result(state, lhs); + + if (bfs_expr_never_returns(lhs)) { + opt_debug(state, 1, "reachability: %pe <==> %pe\n", expr, lhs); + opt_warning(state, expr->rhs, "This expression is unreachable.\n\n"); + return extract_child_expr(expr, &expr->lhs); + } else if ((lhs->always_true && rhs == &bfs_true) + || (lhs->always_false && rhs == &bfs_false)) { + opt_debug(state, 1, "redundancy elimination: %pe <==> %pe\n", expr, lhs); + return extract_child_expr(expr, &expr->lhs); + } else if (optlevel >= 2 && lhs->pure) { + opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs); + opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n"); + return extract_child_expr(expr, &expr->rhs); + } + } + + expr->pure = lhs->pure && rhs->pure; + expr->always_true = bfs_expr_never_returns(lhs) || rhs->always_true; + expr->always_false = bfs_expr_never_returns(lhs) || rhs->always_false; + expr->cost = lhs->cost + rhs->cost; + expr->probability = rhs->probability; + + return expr; +} + +/** Optimize a comma expression recursively. */ +static struct bfs_expr *optimize_comma_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { + struct opt_state lhs_state = *state; + expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs); + if (!expr->lhs) { + goto fail; + } + + struct opt_state rhs_state = *state; + facts_union(&rhs_state.facts, &lhs_state.facts_when_true, &lhs_state.facts_when_false); + expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); + if (!expr->rhs) { + goto fail; + } + + return optimize_comma_expr(state, expr); + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** Infer data flow facts about a predicate. */ +static void infer_pred_facts(struct opt_state *state, enum pred_type pred) { + constrain_pred(&state->facts_when_true.preds[pred], true); + constrain_pred(&state->facts_when_false.preds[pred], false); +} + +/** Infer data flow facts about an -{execut,read,writ}able expression. */ +static void infer_access_facts(struct opt_state *state, const struct bfs_expr *expr) { + if (expr->num & R_OK) { + infer_pred_facts(state, READABLE_PRED); + } + if (expr->num & W_OK) { + infer_pred_facts(state, WRITABLE_PRED); + } + if (expr->num & X_OK) { + infer_pred_facts(state, EXECUTABLE_PRED); + } +} + +/** Infer data flow facts about an icmp-style ([+-]N) expression. */ +static void infer_icmp_facts(struct opt_state *state, const struct bfs_expr *expr, enum range_type type) { + struct range *range_when_true = &state->facts_when_true.ranges[type]; + struct range *range_when_false = &state->facts_when_false.ranges[type]; + long long value = expr->num; + + switch (expr->int_cmp) { + case BFS_INT_EQUAL: + constrain_min(range_when_true, value); + constrain_max(range_when_true, value); + range_remove(range_when_false, value); + break; + + case BFS_INT_LESS: + constrain_min(range_when_false, value); + constrain_max(range_when_true, value); + range_remove(range_when_true, value); + break; + + case BFS_INT_GREATER: + constrain_max(range_when_false, value); + constrain_min(range_when_true, value); + range_remove(range_when_true, value); + break; + } +} + +/** Infer data flow facts about a -gid expression. */ +static void infer_gid_facts(struct opt_state *state, const struct bfs_expr *expr) { + infer_icmp_facts(state, expr, GID_RANGE); + + const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); + struct range *range = &state->facts_when_true.ranges[GID_RANGE]; + if (groups && range->min == range->max) { + gid_t gid = range->min; + bool nogroup = !bfs_getgrgid(groups, gid); + constrain_pred(&state->facts_when_true.preds[NOGROUP_PRED], nogroup); + } +} + +/** Infer data flow facts about a -uid expression. */ +static void infer_uid_facts(struct opt_state *state, const struct bfs_expr *expr) { + infer_icmp_facts(state, expr, UID_RANGE); + + const struct bfs_users *users = bfs_ctx_users(state->ctx); + struct range *range = &state->facts_when_true.ranges[UID_RANGE]; + if (users && range->min == range->max) { + uid_t uid = range->min; + bool nouser = !bfs_getpwuid(users, uid); + constrain_pred(&state->facts_when_true.preds[NOUSER_PRED], nouser); + } +} + +/** Infer data flow facts about a -samefile expression. */ +static void infer_samefile_facts(struct opt_state *state, const struct bfs_expr *expr) { + struct range *range_when_true = &state->facts_when_true.ranges[INUM_RANGE]; + constrain_min(range_when_true, expr->ino); + constrain_max(range_when_true, expr->ino); +} + +/** Infer data flow facts about a -type expression. */ +static void infer_type_facts(struct opt_state *state, const struct bfs_expr *expr) { + state->facts_when_true.types &= expr->num; + state->facts_when_false.types &= ~expr->num; +} + +/** Infer data flow facts about an -xtype expression. */ +static void infer_xtype_facts(struct opt_state *state, const struct bfs_expr *expr) { + state->facts_when_true.xtypes &= expr->num; + state->facts_when_false.xtypes &= ~expr->num; +} + +static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { + int optlevel = state->ctx->optlevel; + + state->facts_when_true = state->facts; + state->facts_when_false = state->facts; + + if (optlevel >= 2 && facts_are_impossible(&state->facts)) { + opt_debug(state, 2, "reachability: %pe --> %pe\n", expr, &bfs_false); + opt_warning(state, expr, "This expression is unreachable.\n\n"); + bfs_expr_free(expr); + expr = &bfs_false; + goto done; + } + + if (!bfs_expr_has_children(expr) && !expr->pure) { + facts_union(state->facts_when_impure, state->facts_when_impure, &state->facts); + } + + if (expr->eval_fn == eval_access) { + infer_access_facts(state, expr); + } else if (expr->eval_fn == eval_acl) { + infer_pred_facts(state, ACL_PRED); + } else if (expr->eval_fn == eval_capable) { + infer_pred_facts(state, CAPABLE_PRED); + } else if (expr->eval_fn == eval_depth) { + infer_icmp_facts(state, expr, DEPTH_RANGE); + } else if (expr->eval_fn == eval_empty) { + infer_pred_facts(state, EMPTY_PRED); + } else if (expr->eval_fn == eval_gid) { + infer_gid_facts(state, expr); + } else if (expr->eval_fn == eval_hidden) { + infer_pred_facts(state, HIDDEN_PRED); + } else if (expr->eval_fn == eval_inum) { + infer_icmp_facts(state, expr, INUM_RANGE); + } else if (expr->eval_fn == eval_links) { + infer_icmp_facts(state, expr, LINKS_RANGE); + } else if (expr->eval_fn == eval_nogroup) { + infer_pred_facts(state, NOGROUP_PRED); + } else if (expr->eval_fn == eval_nouser) { + infer_pred_facts(state, NOUSER_PRED); + } else if (expr->eval_fn == eval_samefile) { + infer_samefile_facts(state, expr); + } else if (expr->eval_fn == eval_size) { + infer_icmp_facts(state, expr, SIZE_RANGE); + } else if (expr->eval_fn == eval_sparse) { + infer_pred_facts(state, SPARSE_PRED); + } else if (expr->eval_fn == eval_type) { + infer_type_facts(state, expr); + } else if (expr->eval_fn == eval_uid) { + infer_uid_facts(state, expr); + } else if (expr->eval_fn == eval_xattr) { + infer_pred_facts(state, XATTR_PRED); + } else if (expr->eval_fn == eval_xtype) { + infer_xtype_facts(state, expr); + } else if (expr->eval_fn == eval_not) { + expr = optimize_not_expr_recursive(state, expr); + } else if (expr->eval_fn == eval_and) { + expr = optimize_and_expr_recursive(state, expr); + } else if (expr->eval_fn == eval_or) { + expr = optimize_or_expr_recursive(state, expr); + } else if (expr->eval_fn == eval_comma) { + expr = optimize_comma_expr_recursive(state, expr); + } + + if (!expr) { + goto done; + } + + if (bfs_expr_has_children(expr)) { + struct bfs_expr *lhs = expr->lhs; + struct bfs_expr *rhs = expr->rhs; + if (rhs) { + expr->persistent_fds = rhs->persistent_fds; + expr->ephemeral_fds = rhs->ephemeral_fds; + } + if (lhs) { + expr->persistent_fds += lhs->persistent_fds; + if (lhs->ephemeral_fds > expr->ephemeral_fds) { + expr->ephemeral_fds = lhs->ephemeral_fds; + } + } + } + + if (expr->always_true) { + set_facts_impossible(&state->facts_when_false); + } + if (expr->always_false) { + set_facts_impossible(&state->facts_when_true); + } + + if (optlevel < 2 || expr == &bfs_true || expr == &bfs_false) { + goto done; + } + + if (facts_are_impossible(&state->facts_when_true)) { + if (expr->pure) { + opt_debug(state, 2, "data flow: %pe --> %pe\n", expr, &bfs_false); + opt_warning(state, expr, "This expression is always false.\n\n"); + bfs_expr_free(expr); + expr = &bfs_false; + } else { + expr->always_false = true; + expr->probability = 0.0; + } + } else if (facts_are_impossible(&state->facts_when_false)) { + if (expr->pure) { + opt_debug(state, 2, "data flow: %pe --> %pe\n", expr, &bfs_true); + opt_warning(state, expr, "This expression is always true.\n\n"); + bfs_expr_free(expr); + expr = &bfs_true; + } else { + expr->always_true = true; + expr->probability = 1.0; + } + } + +done: + return expr; +} + +/** Swap the children of a binary expression if it would reduce the cost. */ +static bool reorder_expr(const struct opt_state *state, struct bfs_expr *expr, float swapped_cost) { + if (swapped_cost < expr->cost) { + bool debug = opt_debug(state, 3, "cost: %pe <==> ", expr); + struct bfs_expr *lhs = expr->lhs; + expr->lhs = expr->rhs; + expr->rhs = lhs; + if (debug) { + cfprintf(state->ctx->cerr, "%pe (~${ylw}%g${rs} --> ~${ylw}%g${rs})\n", expr, expr->cost, swapped_cost); + } + expr->cost = swapped_cost; + return true; + } else { + return false; + } +} + +/** + * Recursively reorder sub-expressions to reduce the overall cost. + * + * @param expr + * The expression to optimize. + * @return + * Whether any subexpression was reordered. + */ +static bool reorder_expr_recursive(const struct opt_state *state, struct bfs_expr *expr) { + if (!bfs_expr_has_children(expr)) { + return false; + } + + struct bfs_expr *lhs = expr->lhs; + struct bfs_expr *rhs = expr->rhs; + + bool ret = false; + if (lhs) { + ret |= reorder_expr_recursive(state, lhs); + } + if (rhs) { + ret |= reorder_expr_recursive(state, rhs); + } + + if (expr->eval_fn == eval_and || expr->eval_fn == eval_or) { + if (lhs->pure && rhs->pure) { + float rhs_prob = expr->eval_fn == eval_and ? rhs->probability : 1.0 - rhs->probability; + float swapped_cost = rhs->cost + rhs_prob*lhs->cost; + ret |= reorder_expr(state, expr, swapped_cost); + } + } + + return ret; +} + +/** + * Optimize a top-level expression. + */ +static struct bfs_expr *optimize_expr(struct opt_state *state, struct bfs_expr *expr) { + struct opt_facts saved_impure = *state->facts_when_impure; + + expr = optimize_expr_recursive(state, expr); + if (!expr) { + return NULL; + } + + if (state->ctx->optlevel >= 3 && reorder_expr_recursive(state, expr)) { + // Re-do optimizations to account for the new ordering + *state->facts_when_impure = saved_impure; + expr = optimize_expr_recursive(state, expr); + if (!expr) { + return NULL; + } + } + + return expr; +} + +int bfs_optimize(struct bfs_ctx *ctx) { + bfs_ctx_dump(ctx, DEBUG_OPT); + + struct opt_facts facts_when_impure; + set_facts_impossible(&facts_when_impure); + + struct opt_state state = { + .ctx = ctx, + .facts_when_impure = &facts_when_impure, + }; + facts_init(&state.facts); + + ctx->exclude = optimize_expr(&state, ctx->exclude); + if (!ctx->exclude) { + return -1; + } + + // Only non-excluded files are evaluated + state.facts = state.facts_when_false; + + struct range *depth = &state.facts.ranges[DEPTH_RANGE]; + constrain_min(depth, ctx->mindepth); + constrain_max(depth, ctx->maxdepth); + + ctx->expr = optimize_expr(&state, ctx->expr); + if (!ctx->expr) { + return -1; + } + + ctx->expr = ignore_result(&state, ctx->expr); + + if (facts_are_impossible(&facts_when_impure)) { + bfs_warning(ctx, "This command won't do anything.\n\n"); + } + + const struct range *depth_when_impure = &facts_when_impure.ranges[DEPTH_RANGE]; + long long mindepth = depth_when_impure->min; + long long maxdepth = depth_when_impure->max; + + int optlevel = ctx->optlevel; + + if (optlevel >= 2 && mindepth > ctx->mindepth) { + if (mindepth > INT_MAX) { + mindepth = INT_MAX; + } + ctx->mindepth = mindepth; + opt_debug(&state, 2, "data flow: mindepth --> %d\n", ctx->mindepth); + } + + if (optlevel >= 4 && maxdepth < ctx->maxdepth) { + if (maxdepth < INT_MIN) { + maxdepth = INT_MIN; + } + ctx->maxdepth = maxdepth; + opt_debug(&state, 4, "data flow: maxdepth --> %d\n", ctx->maxdepth); + } + + return 0; +} diff --git a/src/opt.h b/src/opt.h new file mode 100644 index 0000000..5f8180d --- /dev/null +++ b/src/opt.h @@ -0,0 +1,37 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020 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. * + ****************************************************************************/ + +/** + * Optimization. + */ + +#ifndef BFS_OPT_H +#define BFS_OPT_H + +struct bfs_ctx; + +/** + * Apply optimizations to the command line. + * + * @param ctx + * The bfs context to optimize. + * @return + * 0 if successful, -1 on error. + */ +int bfs_optimize(struct bfs_ctx *ctx); + +#endif // BFS_OPT_H + diff --git a/src/parse.c b/src/parse.c new file mode 100644 index 0000000..65087a0 --- /dev/null +++ b/src/parse.c @@ -0,0 +1,3959 @@ +/**************************************************************************** + * 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. * + ****************************************************************************/ + +/** + * The command line parser. Expressions are parsed by recursive descent, with a + * grammar described in the comments of the parse_*() functions. The parser + * also accepts flags and paths at any point in the expression, by treating + * flags like always-true options, and skipping over paths wherever they appear. + */ + +#include "parse.h" +#include "bfs.h" +#include "bftw.h" +#include "color.h" +#include "ctx.h" +#include "darray.h" +#include "diag.h" +#include "dir.h" +#include "eval.h" +#include "exec.h" +#include "expr.h" +#include "fsade.h" +#include "opt.h" +#include "printf.h" +#include "pwcache.h" +#include "stat.h" +#include "typo.h" +#include "util.h" +#include "xregex.h" +#include "xspawn.h" +#include "xtime.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <fnmatch.h> +#include <grp.h> +#include <limits.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/time.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +// Strings printed by -D tree for "fake" expressions +static char *fake_and_arg = "-a"; +static char *fake_false_arg = "-false"; +static char *fake_hidden_arg = "-hidden"; +static char *fake_or_arg = "-o"; +static char *fake_print_arg = "-print"; +static char *fake_true_arg = "-true"; + +// Cost estimation constants +#define FAST_COST 40.0 +#define STAT_COST 1000.0 +#define PRINT_COST 20000.0 + +struct bfs_expr bfs_true = { + .eval_fn = eval_true, + .argc = 1, + .argv = &fake_true_arg, + .pure = true, + .always_true = true, + .synthetic = true, + .cost = FAST_COST, + .probability = 1.0, +}; + +struct bfs_expr bfs_false = { + .eval_fn = eval_false, + .argc = 1, + .argv = &fake_false_arg, + .pure = true, + .always_false = true, + .synthetic = true, + .cost = FAST_COST, + .probability = 0.0, +}; + +void bfs_expr_free(struct bfs_expr *expr) { + if (!expr || expr == &bfs_true || expr == &bfs_false) { + return; + } + + if (bfs_expr_has_children(expr)) { + bfs_expr_free(expr->rhs); + bfs_expr_free(expr->lhs); + } else if (expr->eval_fn == eval_exec) { + bfs_exec_free(expr->exec); + } else if (expr->eval_fn == eval_fprintf) { + bfs_printf_free(expr->printf); + } else if (expr->eval_fn == eval_regex) { + bfs_regfree(expr->regex); + } + + free(expr); +} + +struct bfs_expr *bfs_expr_new(bfs_eval_fn *eval_fn, size_t argc, char **argv) { + struct bfs_expr *expr = malloc(sizeof(*expr)); + if (!expr) { + perror("malloc()"); + return NULL; + } + + expr->eval_fn = eval_fn; + expr->argc = argc; + expr->argv = argv; + expr->persistent_fds = 0; + expr->ephemeral_fds = 0; + expr->pure = false; + expr->always_true = false; + expr->always_false = false; + expr->synthetic = false; + expr->cost = FAST_COST; + expr->probability = 0.5; + expr->evaluations = 0; + expr->successes = 0; + expr->elapsed.tv_sec = 0; + expr->elapsed.tv_nsec = 0; + return expr; +} + +/** + * Create a new unary expression. + */ +static struct bfs_expr *new_unary_expr(bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) { + struct bfs_expr *expr = bfs_expr_new(eval_fn, 1, argv); + if (!expr) { + bfs_expr_free(rhs); + return NULL; + } + + expr->lhs = NULL; + expr->rhs = rhs; + assert(bfs_expr_has_children(expr)); + + expr->persistent_fds = rhs->persistent_fds; + expr->ephemeral_fds = rhs->ephemeral_fds; + return expr; +} + +/** + * Create a new binary expression. + */ +static struct bfs_expr *new_binary_expr(bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) { + struct bfs_expr *expr = bfs_expr_new(eval_fn, 1, argv); + if (!expr) { + bfs_expr_free(rhs); + bfs_expr_free(lhs); + return NULL; + } + + expr->lhs = lhs; + expr->rhs = rhs; + assert(bfs_expr_has_children(expr)); + + if (argv == &fake_and_arg || argv == &fake_or_arg) { + expr->synthetic = true; + } + + expr->persistent_fds = lhs->persistent_fds + rhs->persistent_fds; + if (lhs->ephemeral_fds > rhs->ephemeral_fds) { + expr->ephemeral_fds = lhs->ephemeral_fds; + } else { + expr->ephemeral_fds = rhs->ephemeral_fds; + } + + return expr; +} + +bool bfs_expr_has_children(const struct bfs_expr *expr) { + return expr->eval_fn == eval_and + || expr->eval_fn == eval_or + || expr->eval_fn == eval_not + || expr->eval_fn == eval_comma; +} + +bool bfs_expr_never_returns(const struct bfs_expr *expr) { + // Expressions that never return are vacuously both always true and always false + return expr->always_true && expr->always_false; +} + +/** + * Set an expression to always return true. + */ +static void expr_set_always_true(struct bfs_expr *expr) { + expr->always_true = true; + expr->probability = 1.0; +} + +/** + * Set an expression to never return. + */ +static void expr_set_never_returns(struct bfs_expr *expr) { + expr->always_true = expr->always_false = true; +} + +/** + * Color use flags. + */ +enum use_color { + COLOR_NEVER, + COLOR_AUTO, + COLOR_ALWAYS, +}; + +/** + * Ephemeral state for parsing the command line. + */ +struct parser_state { + /** The command line being constructed. */ + struct bfs_ctx *ctx; + /** The command line arguments being parsed. */ + char **argv; + /** The name of this program. */ + const char *command; + + /** The current regex flags to use. */ + enum bfs_regex_type regex_type; + + /** Whether stdout is a terminal. */ + bool stdout_tty; + /** Whether this session is interactive (stdin and stderr are each a terminal). */ + bool interactive; + /** Whether -color or -nocolor has been passed. */ + enum use_color use_color; + /** Whether a -print action is implied. */ + bool implicit_print; + /** Whether the default root "." should be used. */ + bool implicit_root; + /** Whether the expression has started. */ + bool expr_started; + /** Whether an information option like -help or -version was passed. */ + bool just_info; + /** Whether we are currently parsing an -exclude expression. */ + bool excluding; + + /** The last non-path argument. */ + char **last_arg; + /** A "-depth"-type argument, if any. */ + char **depth_arg; + /** A "-prune" argument, if any. */ + char **prune_arg; + /** A "-mount" argument, if any. */ + char **mount_arg; + /** An "-xdev" argument, if any. */ + char **xdev_arg; + /** A "-files0-from" argument, if any. */ + char **files0_arg; + /** A "-files0-from -" argument, if any. */ + char **files0_stdin_arg; + /** An "-ok"-type expression, if any. */ + const struct bfs_expr *ok_expr; + + /** The current time. */ + struct timespec now; +}; + +/** + * Possible token types. + */ +enum token_type { + /** A flag. */ + T_FLAG, + /** A root path. */ + T_PATH, + /** An option. */ + T_OPTION, + /** A test. */ + T_TEST, + /** An action. */ + T_ACTION, + /** An operator. */ + T_OPERATOR, +}; + +/** + * Print a low-level error message during parsing. + */ +static void parse_perror(const struct parser_state *state, const char *str) { + bfs_perror(state->ctx, str); +} + +/** Initialize an empty highlighted range. */ +static void init_highlight(const struct bfs_ctx *ctx, bool *args) { + for (size_t i = 0; i < ctx->argc; ++i) { + args[i] = false; + } +} + +/** Highlight a range of command line arguments. */ +static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, bool *args) { + size_t i = argv - ctx->argv; + for (size_t j = 0; j < argc; ++j) { + assert(i + j < ctx->argc); + args[i + j] = true; + } +} + +/** + * Print an error message during parsing. + */ +BFS_FORMATTER(2, 3) +static void parse_error(const struct parser_state *state, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, state->argv, 1, highlight); + bfs_argv_error(ctx, highlight); + + va_list args; + va_start(args, format); + errno = error; + bfs_verror(state->ctx, format, args); + va_end(args); +} + +/** + * Print an error about some command line arguments. + */ +BFS_FORMATTER(4, 5) +static void parse_argv_error(const struct parser_state *state, char **argv, size_t argc, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, argv, argc, highlight); + bfs_argv_error(ctx, highlight); + + va_list args; + va_start(args, format); + errno = error; + bfs_verror(ctx, format, args); + va_end(args); +} + +/** + * Print an error about conflicting command line arguments. + */ +BFS_FORMATTER(6, 7) +static void parse_conflict_error(const struct parser_state *state, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, argv1, argc1, highlight); + highlight_args(ctx, argv2, argc2, highlight); + bfs_argv_error(ctx, highlight); + + va_list args; + va_start(args, format); + errno = error; + bfs_verror(ctx, format, args); + va_end(args); +} + +/** + * Print an error about an expression. + */ +BFS_FORMATTER(3, 4) +static void parse_expr_error(const struct parser_state *state, const struct bfs_expr *expr, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + bfs_expr_error(ctx, expr); + + va_list args; + va_start(args, format); + errno = error; + bfs_verror(ctx, format, args); + va_end(args); +} + +/** + * Print a warning message during parsing. + */ +BFS_FORMATTER(2, 3) +static bool parse_warning(const struct parser_state *state, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, state->argv, 1, highlight); + if (!bfs_argv_warning(ctx, highlight)) { + return false; + } + + va_list args; + va_start(args, format); + errno = error; + bool ret = bfs_vwarning(state->ctx, format, args); + va_end(args); + return ret; +} + +/** + * Print a warning about conflicting command line arguments. + */ +BFS_FORMATTER(6, 7) +static bool parse_conflict_warning(const struct parser_state *state, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, argv1, argc1, highlight); + highlight_args(ctx, argv2, argc2, highlight); + if (!bfs_argv_warning(ctx, highlight)) { + return false; + } + + va_list args; + va_start(args, format); + errno = error; + bool ret = bfs_vwarning(ctx, format, args); + va_end(args); + return ret; +} + +/** + * Print a warning about an expression. + */ +BFS_FORMATTER(3, 4) +static bool parse_expr_warning(const struct parser_state *state, const struct bfs_expr *expr, const char *format, ...) { + int error = errno; + const struct bfs_ctx *ctx = state->ctx; + + if (!bfs_expr_warning(ctx, expr)) { + return false; + } + + va_list args; + va_start(args, format); + errno = error; + bool ret = bfs_vwarning(ctx, format, args); + va_end(args); + return ret; +} + +/** + * Fill in a "-print"-type expression. + */ +static void init_print_expr(struct parser_state *state, struct bfs_expr *expr) { + expr_set_always_true(expr); + expr->cost = PRINT_COST; + expr->cfile = state->ctx->cout; +} + +/** + * Open a file for an expression. + */ +static int expr_open(struct parser_state *state, struct bfs_expr *expr, const char *path) { + struct bfs_ctx *ctx = state->ctx; + + FILE *file = NULL; + CFILE *cfile = NULL; + + file = xfopen(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC); + if (!file) { + goto fail; + } + + cfile = cfwrap(file, state->use_color ? ctx->colors : NULL, true); + if (!cfile) { + goto fail; + } + + CFILE *dedup = bfs_ctx_dedup(ctx, cfile, path); + if (!dedup) { + goto fail; + } + + if (dedup != cfile) { + cfclose(cfile); + } + + expr->cfile = dedup; + return 0; + +fail: + parse_expr_error(state, expr, "%m.\n"); + if (cfile) { + cfclose(cfile); + } else if (file) { + fclose(file); + } + return -1; +} + +/** + * Invoke bfs_stat() on an argument. + */ +static int stat_arg(const struct parser_state *state, char **arg, struct bfs_stat *sb) { + const struct bfs_ctx *ctx = state->ctx; + + bool follow = ctx->flags & (BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL); + enum bfs_stat_flags flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW; + + int ret = bfs_stat(AT_FDCWD, *arg, flags, sb); + if (ret != 0) { + parse_argv_error(state, arg, 1, "%m.\n"); + } + return ret; +} + +/** + * Parse the expression specified on the command line. + */ +static struct bfs_expr *parse_expr(struct parser_state *state); + +/** + * Advance by a single token. + */ +static char **parser_advance(struct parser_state *state, enum token_type type, size_t argc) { + if (type != T_FLAG && type != T_PATH) { + state->expr_started = true; + } + + if (type != T_PATH) { + state->last_arg = state->argv; + } + + char **argv = state->argv; + state->argv += argc; + return argv; +} + +/** + * Parse a root path. + */ +static int parse_root(struct parser_state *state, const char *path) { + char *copy = strdup(path); + if (!copy) { + parse_perror(state, "strdup()"); + return -1; + } + + struct bfs_ctx *ctx = state->ctx; + if (DARRAY_PUSH(&ctx->paths, ©) != 0) { + parse_perror(state, "DARRAY_PUSH()"); + free(copy); + return -1; + } + + state->implicit_root = false; + return 0; +} + +/** + * While parsing an expression, skip any paths and add them to ctx->paths. + */ +static int skip_paths(struct parser_state *state) { + while (true) { + const char *arg = state->argv[0]; + if (!arg) { + return 0; + } + + if (arg[0] == '-') { + if (strcmp(arg, "--") == 0) { + // find uses -- to separate flags from the rest + // of the command line. We allow mixing flags + // and paths/predicates, so we just ignore --. + parser_advance(state, T_FLAG, 1); + continue; + } + if (strcmp(arg, "-") != 0) { + // - by itself is a file name. Anything else + // starting with - is a flag/predicate. + return 0; + } + } + + // By POSIX, these are always options + if (strcmp(arg, "(") == 0 || strcmp(arg, "!") == 0) { + return 0; + } + + if (state->expr_started) { + // By POSIX, these can be paths. We only treat them as + // such at the beginning of the command line. + if (strcmp(arg, ")") == 0 || strcmp(arg, ",") == 0) { + return 0; + } + } + + if (parse_root(state, arg) != 0) { + return -1; + } + + parser_advance(state, T_PATH, 1); + } +} + +/** Integer parsing flags. */ +enum int_flags { + IF_BASE_MASK = 0x03F, + IF_INT = 0x040, + IF_LONG = 0x080, + IF_LONG_LONG = 0x0C0, + IF_SIZE_MASK = 0x0C0, + IF_UNSIGNED = 0x100, + IF_PARTIAL_OK = 0x200, + IF_QUIET = 0x400, +}; + +/** + * Parse an integer. + */ +static const char *parse_int(const struct parser_state *state, char **arg, const char *str, void *result, enum int_flags flags) { + char *endptr; + + int base = flags & IF_BASE_MASK; + if (base == 0) { + base = 10; + } + + errno = 0; + long long value = strtoll(str, &endptr, base); + if (errno != 0) { + if (errno == ERANGE) { + goto range; + } else { + goto bad; + } + } + + if (endptr == str) { + goto bad; + } + + if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') { + goto bad; + } + + if ((flags & IF_UNSIGNED) && value < 0) { + goto negative; + } + + switch (flags & IF_SIZE_MASK) { + case IF_INT: + if (value < INT_MIN || value > INT_MAX) { + goto range; + } + *(int *)result = value; + break; + + case IF_LONG: + if (value < LONG_MIN || value > LONG_MAX) { + goto range; + } + *(long *)result = value; + break; + + case IF_LONG_LONG: + *(long long *)result = value; + break; + + default: + assert(!"Invalid int size"); + goto bad; + } + + return endptr; + +bad: + if (!(flags & IF_QUIET)) { + parse_argv_error(state, arg, 1, "${bld}%s${rs} is not a valid integer.\n", str); + } + return NULL; + +negative: + if (!(flags & IF_QUIET)) { + parse_argv_error(state, arg, 1, "Negative integer ${bld}%s${rs} is not allowed here.\n", str); + } + return NULL; + +range: + if (!(flags & IF_QUIET)) { + parse_argv_error(state, arg, 1, "${bld}%s${rs} is too large an integer.\n", str); + } + return NULL; +} + +/** + * Parse an integer and a comparison flag. + */ +static const char *parse_icmp(const struct parser_state *state, struct bfs_expr *expr, enum int_flags flags) { + char **arg = &expr->argv[1]; + const char *str = *arg; + switch (str[0]) { + case '-': + expr->int_cmp = BFS_INT_LESS; + ++str; + break; + case '+': + expr->int_cmp = BFS_INT_GREATER; + ++str; + break; + default: + expr->int_cmp = BFS_INT_EQUAL; + break; + } + + return parse_int(state, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED); +} + +/** + * Check if a string could be an integer comparison. + */ +static bool looks_like_icmp(const char *str) { + int i; + + // One +/- for the comparison flag, one for the sign + for (i = 0; i < 2; ++i) { + if (str[i] != '-' && str[i] != '+') { + break; + } + } + + return str[i] >= '0' && str[i] <= '9'; +} + +/** + * Parse a single flag. + */ +static struct bfs_expr *parse_flag(struct parser_state *state, size_t argc) { + parser_advance(state, T_FLAG, argc); + return &bfs_true; +} + +/** + * Parse a flag that doesn't take a value. + */ +static struct bfs_expr *parse_nullary_flag(struct parser_state *state) { + return parse_flag(state, 1); +} + +/** + * Parse a single option. + */ +static struct bfs_expr *parse_option(struct parser_state *state, size_t argc) { + parser_advance(state, T_OPTION, argc); + return &bfs_true; +} + +/** + * Parse an option that doesn't take a value. + */ +static struct bfs_expr *parse_nullary_option(struct parser_state *state) { + return parse_option(state, 1); +} + +/** + * Parse an option that takes a value. + */ +static struct bfs_expr *parse_unary_option(struct parser_state *state) { + return parse_option(state, 2); +} + +/** + * Parse a single test. + */ +static struct bfs_expr *parse_test(struct parser_state *state, bfs_eval_fn *eval_fn, size_t argc) { + char **argv = parser_advance(state, T_TEST, argc); + struct bfs_expr *expr = bfs_expr_new(eval_fn, argc, argv); + if (expr) { + expr->pure = true; + } + return expr; +} + +/** + * Parse a test that doesn't take a value. + */ +static struct bfs_expr *parse_nullary_test(struct parser_state *state, bfs_eval_fn *eval_fn) { + return parse_test(state, eval_fn, 1); +} + +/** + * Parse a test that takes a value. + */ +static struct bfs_expr *parse_unary_test(struct parser_state *state, bfs_eval_fn *eval_fn) { + const char *arg = state->argv[0]; + const char *value = state->argv[1]; + if (!value) { + parse_error(state, "${blu}%s${rs} needs a value.\n", arg); + return NULL; + } + + return parse_test(state, eval_fn, 2); +} + +/** + * Parse a single action. + */ +static struct bfs_expr *parse_action(struct parser_state *state, bfs_eval_fn *eval_fn, size_t argc) { + char **argv = parser_advance(state, T_ACTION, argc); + + if (state->excluding) { + parse_argv_error(state, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n"); + return NULL; + } + + if (eval_fn != eval_prune && eval_fn != eval_quit) { + state->implicit_print = false; + } + + return bfs_expr_new(eval_fn, argc, argv); +} + +/** + * Parse an action that takes no arguments. + */ +static struct bfs_expr *parse_nullary_action(struct parser_state *state, bfs_eval_fn *eval_fn) { + return parse_action(state, eval_fn, 1); +} + +/** + * Parse an action that takes one argument. + */ +static struct bfs_expr *parse_unary_action(struct parser_state *state, bfs_eval_fn *eval_fn) { + const char *arg = state->argv[0]; + const char *value = state->argv[1]; + if (!value) { + parse_error(state, "${blu}%s${rs} needs a value.\n", arg); + return NULL; + } + + return parse_action(state, eval_fn, 2); +} + +/** + * Add an expression to the exclusions. + */ +static int parse_exclude(struct parser_state *state, struct bfs_expr *expr) { + struct bfs_ctx *ctx = state->ctx; + ctx->exclude = new_binary_expr(eval_or, ctx->exclude, expr, &fake_or_arg); + if (ctx->exclude) { + return 0; + } else { + return -1; + } +} + +/** + * Parse a test expression with integer data and a comparison flag. + */ +static struct bfs_expr *parse_test_icmp(struct parser_state *state, bfs_eval_fn *eval_fn) { + struct bfs_expr *expr = parse_unary_test(state, eval_fn); + if (!expr) { + return NULL; + } + + if (!parse_icmp(state, expr, 0)) { + bfs_expr_free(expr); + return NULL; + } + + return expr; +} + +/** + * Print usage information for -D. + */ +static void debug_help(CFILE *cfile) { + cfprintf(cfile, "Supported debug flags:\n\n"); + + cfprintf(cfile, " ${bld}help${rs}: This message.\n"); + cfprintf(cfile, " ${bld}cost${rs}: Show cost estimates.\n"); + cfprintf(cfile, " ${bld}exec${rs}: Print executed command details.\n"); + cfprintf(cfile, " ${bld}opt${rs}: Print optimization details.\n"); + cfprintf(cfile, " ${bld}rates${rs}: Print predicate success rates.\n"); + cfprintf(cfile, " ${bld}search${rs}: Trace the filesystem traversal.\n"); + cfprintf(cfile, " ${bld}stat${rs}: Trace all stat() calls.\n"); + cfprintf(cfile, " ${bld}tree${rs}: Print the parse tree.\n"); + cfprintf(cfile, " ${bld}all${rs}: All debug flags at once.\n"); +} + +/** Check if a substring matches a debug flag. */ +static bool parse_debug_flag(const char *flag, size_t len, const char *expected) { + if (len == strlen(expected)) { + return strncmp(flag, expected, len) == 0; + } else { + return false; + } +} + +/** + * Parse -D FLAG. + */ +static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int arg2) { + struct bfs_ctx *ctx = state->ctx; + + const char *arg = state->argv[0]; + const char *flags = state->argv[1]; + if (!flags) { + parse_error(state, "${cyn}%s${rs} needs a flag.\n\n", arg); + debug_help(ctx->cerr); + return NULL; + } + + parser_advance(state, T_FLAG, 1); + + bool unrecognized = false; + + for (const char *flag = flags, *next; flag; flag = next) { + size_t len = strcspn(flag, ","); + if (flag[len]) { + next = flag + len + 1; + } else { + next = NULL; + } + + if (parse_debug_flag(flag, len, "help")) { + debug_help(ctx->cout); + state->just_info = true; + return NULL; + } else if (parse_debug_flag(flag, len, "all")) { + ctx->debug = DEBUG_ALL; + continue; + } + + enum debug_flags i; + for (i = 1; DEBUG_ALL & i; i <<= 1) { + const char *name = debug_flag_name(i); + if (parse_debug_flag(flag, len, name)) { + break; + } + } + + if (DEBUG_ALL & i) { + ctx->debug |= i; + } else { + if (parse_warning(state, "Unrecognized debug flag ${bld}")) { + fwrite(flag, 1, len, stderr); + cfprintf(ctx->cerr, "${rs}.\n\n"); + unrecognized = true; + } + } + } + + if (unrecognized) { + debug_help(ctx->cerr); + cfprintf(ctx->cerr, "\n"); + } + + parser_advance(state, T_FLAG, 1); + return &bfs_true; +} + +/** + * Parse -On. + */ +static struct bfs_expr *parse_optlevel(struct parser_state *state, int arg1, int arg2) { + int *optlevel = &state->ctx->optlevel; + + if (strcmp(state->argv[0], "-Ofast") == 0) { + *optlevel = 4; + } else if (!parse_int(state, state->argv, state->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) { + return NULL; + } + + if (*optlevel > 4) { + parse_warning(state, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", state->argv[0] + 2); + } + + return parse_nullary_flag(state); +} + +/** + * Parse -[PHL], -follow. + */ +static struct bfs_expr *parse_follow(struct parser_state *state, int flags, int option) { + struct bfs_ctx *ctx = state->ctx; + ctx->flags &= ~(BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL); + ctx->flags |= flags; + if (option) { + return parse_nullary_option(state); + } else { + return parse_nullary_flag(state); + } +} + +/** + * Parse -X. + */ +static struct bfs_expr *parse_xargs_safe(struct parser_state *state, int arg1, int arg2) { + state->ctx->xargs_safe = true; + return parse_nullary_flag(state); +} + +/** + * Parse -executable, -readable, -writable + */ +static struct bfs_expr *parse_access(struct parser_state *state, int flag, int arg2) { + struct bfs_expr *expr = parse_nullary_test(state, eval_access); + if (!expr) { + return NULL; + } + + expr->num = flag; + expr->cost = STAT_COST; + + switch (flag) { + case R_OK: + expr->probability = 0.99; + break; + case W_OK: + expr->probability = 0.8; + break; + case X_OK: + expr->probability = 0.2; + break; + } + + return expr; +} + +/** + * Parse -acl. + */ +static struct bfs_expr *parse_acl(struct parser_state *state, int flag, int arg2) { +#if BFS_CAN_CHECK_ACL + struct bfs_expr *expr = parse_nullary_test(state, eval_acl); + if (expr) { + expr->cost = STAT_COST; + expr->probability = 0.00002; + } + return expr; +#else + parse_error(state, "Missing platform support.\n"); + return NULL; +#endif +} + +/** + * Parse -[aBcm]?newer. + */ +static struct bfs_expr *parse_newer(struct parser_state *state, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_newer); + if (!expr) { + return NULL; + } + + struct bfs_stat sb; + if (stat_arg(state, &expr->argv[1], &sb) != 0) { + goto fail; + } + + expr->cost = STAT_COST; + expr->reftime = sb.mtime; + expr->stat_field = field; + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -[aBcm]min. + */ +static struct bfs_expr *parse_min(struct parser_state *state, int field, int arg2) { + struct bfs_expr *expr = parse_test_icmp(state, eval_time); + if (!expr) { + return NULL; + } + + expr->cost = STAT_COST; + expr->reftime = state->now; + expr->stat_field = field; + expr->time_unit = BFS_MINUTES; + return expr; +} + +/** + * Parse -[aBcm]time. + */ +static struct bfs_expr *parse_time(struct parser_state *state, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_time); + if (!expr) { + return NULL; + } + + expr->cost = STAT_COST; + expr->reftime = state->now; + expr->stat_field = field; + + const char *tail = parse_icmp(state, expr, IF_PARTIAL_OK); + if (!tail) { + goto fail; + } + + if (!*tail) { + expr->time_unit = BFS_DAYS; + return expr; + } + + unsigned long long time = expr->num; + expr->num = 0; + + while (true) { + switch (*tail) { + case 'w': + time *= 7; + BFS_FALLTHROUGH; + case 'd': + time *= 24; + BFS_FALLTHROUGH; + case 'h': + time *= 60; + BFS_FALLTHROUGH; + case 'm': + time *= 60; + BFS_FALLTHROUGH; + case 's': + break; + default: + parse_expr_error(state, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail); + goto fail; + } + + expr->num += time; + + if (!*++tail) { + break; + } + + tail = parse_int(state, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED); + if (!tail) { + goto fail; + } + if (!*tail) { + parse_expr_error(state, expr, "Missing time unit.\n"); + goto fail; + } + } + + expr->time_unit = BFS_SECONDS; + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -capable. + */ +static struct bfs_expr *parse_capable(struct parser_state *state, int flag, int arg2) { +#if BFS_CAN_CHECK_CAPABILITIES + struct bfs_expr *expr = parse_nullary_test(state, eval_capable); + if (expr) { + expr->cost = STAT_COST; + expr->probability = 0.000002; + } + return expr; +#else + parse_error(state, "Missing platform support.\n"); + return NULL; +#endif +} + +/** + * Parse -(no)?color. + */ +static struct bfs_expr *parse_color(struct parser_state *state, int color, int arg2) { + struct bfs_ctx *ctx = state->ctx; + struct colors *colors = ctx->colors; + + if (color) { + if (!colors) { + parse_error(state, "%s.\n", strerror(ctx->colors_error)); + return NULL; + } + + state->use_color = COLOR_ALWAYS; + ctx->cout->colors = colors; + ctx->cerr->colors = colors; + } else { + state->use_color = COLOR_NEVER; + ctx->cout->colors = NULL; + ctx->cerr->colors = NULL; + } + + return parse_nullary_option(state); +} + +/** + * Parse -{false,true}. + */ +static struct bfs_expr *parse_const(struct parser_state *state, int value, int arg2) { + parser_advance(state, T_TEST, 1); + return value ? &bfs_true : &bfs_false; +} + +/** + * Parse -daystart. + */ +static struct bfs_expr *parse_daystart(struct parser_state *state, int arg1, int arg2) { + struct tm tm; + if (xlocaltime(&state->now.tv_sec, &tm) != 0) { + parse_perror(state, "xlocaltime()"); + return NULL; + } + + if (tm.tm_hour || tm.tm_min || tm.tm_sec || state->now.tv_nsec) { + ++tm.tm_mday; + } + tm.tm_hour = 0; + tm.tm_min = 0; + tm.tm_sec = 0; + + time_t time; + if (xmktime(&tm, &time) != 0) { + parse_perror(state, "xmktime()"); + return NULL; + } + + state->now.tv_sec = time; + state->now.tv_nsec = 0; + + return parse_nullary_option(state); +} + +/** + * Parse -delete. + */ +static struct bfs_expr *parse_delete(struct parser_state *state, int arg1, int arg2) { + state->ctx->flags |= BFTW_POST_ORDER; + state->depth_arg = state->argv; + return parse_nullary_action(state, eval_delete); +} + +/** + * Parse -d. + */ +static struct bfs_expr *parse_depth(struct parser_state *state, int arg1, int arg2) { + state->ctx->flags |= BFTW_POST_ORDER; + state->depth_arg = state->argv; + return parse_nullary_flag(state); +} + +/** + * Parse -depth [N]. + */ +static struct bfs_expr *parse_depth_n(struct parser_state *state, int arg1, int arg2) { + const char *arg = state->argv[1]; + if (arg && looks_like_icmp(arg)) { + return parse_test_icmp(state, eval_depth); + } else { + return parse_depth(state, arg1, arg2); + } +} + +/** + * Parse -{min,max}depth N. + */ +static struct bfs_expr *parse_depth_limit(struct parser_state *state, int is_min, int arg2) { + struct bfs_ctx *ctx = state->ctx; + const char *arg = state->argv[0]; + const char *value = state->argv[1]; + if (!value) { + parse_error(state, "${blu}%s${rs} needs a value.\n", arg); + return NULL; + } + + int *depth = is_min ? &ctx->mindepth : &ctx->maxdepth; + if (!parse_int(state, &state->argv[1], value, depth, IF_INT | IF_UNSIGNED)) { + return NULL; + } + + return parse_unary_option(state); +} + +/** + * Parse -empty. + */ +static struct bfs_expr *parse_empty(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_test(state, eval_empty); + if (!expr) { + return NULL; + } + + expr->cost = 2000.0; + expr->probability = 0.01; + + if (state->ctx->optlevel < 4) { + // Since -empty attempts to open and read directories, it may + // have side effects such as reporting permission errors, and + // thus shouldn't be re-ordered without aggressive optimizations + expr->pure = false; + } + + expr->ephemeral_fds = 1; + + return expr; +} + +/** + * Parse -exec(dir)?/-ok(dir)?. + */ +static struct bfs_expr *parse_exec(struct parser_state *state, int flags, int arg2) { + struct bfs_exec *execbuf = bfs_exec_parse(state->ctx, state->argv, flags); + if (!execbuf) { + return NULL; + } + + struct bfs_expr *expr = parse_action(state, eval_exec, execbuf->tmpl_argc + 2); + if (!expr) { + bfs_exec_free(execbuf); + return NULL; + } + + expr->exec = execbuf; + + if (execbuf->flags & BFS_EXEC_MULTI) { + expr_set_always_true(expr); + } else { + expr->cost = 1000000.0; + } + + expr->ephemeral_fds = 2; + if (execbuf->flags & BFS_EXEC_CHDIR) { + if (execbuf->flags & BFS_EXEC_MULTI) { + expr->persistent_fds = 1; + } else { + ++expr->ephemeral_fds; + } + } + + if (execbuf->flags & BFS_EXEC_CONFIRM) { + state->ok_expr = expr; + } + + return expr; +} + +/** + * Parse -exit [STATUS]. + */ +static struct bfs_expr *parse_exit(struct parser_state *state, int arg1, int arg2) { + size_t argc = 1; + const char *value = state->argv[1]; + + int status = EXIT_SUCCESS; + if (value && parse_int(state, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) { + argc = 2; + } + + struct bfs_expr *expr = parse_action(state, eval_exit, argc); + if (expr) { + expr_set_never_returns(expr); + expr->num = status; + } + return expr; +} + +/** + * Parse -f PATH. + */ +static struct bfs_expr *parse_f(struct parser_state *state, int arg1, int arg2) { + const char *path = state->argv[1]; + if (!path) { + parse_error(state, "${cyn}-f${rs} requires a path.\n"); + return NULL; + } + + if (parse_root(state, path) != 0) { + return NULL; + } + + parser_advance(state, T_FLAG, 1); + parser_advance(state, T_PATH, 1); + return &bfs_true; +} + +/** + * Parse -files0-from PATH. + */ +static struct bfs_expr *parse_files0_from(struct parser_state *state, int arg1, int arg2) { + const char *arg = state->argv[0]; + const char *from = state->argv[1]; + if (!from) { + parse_error(state, "${blu}%s${rs} requires a path.\n", arg); + return NULL; + } + + state->files0_arg = parser_advance(state, T_OPTION, 1); + + FILE *file; + if (strcmp(from, "-") == 0) { + file = stdin; + } else { + file = xfopen(from, O_RDONLY | O_CLOEXEC); + } + if (!file) { + parse_error(state, "%m.\n"); + return NULL; + } + + struct bfs_expr *expr = &bfs_true; + + while (true) { + char *path = xgetdelim(file, '\0'); + if (!path) { + if (errno) { + parse_error(state, "%m.\n"); + expr = NULL; + } + break; + } + + int ret = parse_root(state, path); + free(path); + if (ret != 0) { + expr = NULL; + break; + } + } + + if (file == stdin) { + state->files0_stdin_arg = state->files0_arg; + } else { + fclose(file); + } + + state->implicit_root = false; + parser_advance(state, T_OPTION, 1); + return expr; +} + +/** + * Parse -flags FLAGS. + */ +static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_flags); + if (!expr) { + return NULL; + } + + const char *flags = expr->argv[1]; + switch (flags[0]) { + case '-': + expr->flags_cmp = BFS_MODE_ALL; + ++flags; + break; + case '+': + expr->flags_cmp = BFS_MODE_ANY; + ++flags; + break; + default: + expr->flags_cmp = BFS_MODE_EQUAL; + break; + } + + if (xstrtofflags(&flags, &expr->set_flags, &expr->clear_flags) != 0) { + if (errno == ENOTSUP) { + parse_expr_error(state, expr, "Missing platform support.\n"); + } else { + parse_expr_error(state, expr, "Invalid flags.\n"); + } + bfs_expr_free(expr); + return NULL; + } + + return expr; +} + +/** + * Parse -fls FILE. + */ +static struct bfs_expr *parse_fls(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(state, eval_fls); + if (!expr) { + goto fail; + } + + if (expr_open(state, expr, expr->argv[1]) != 0) { + goto fail; + } + + expr_set_always_true(expr); + expr->cost = PRINT_COST; + expr->reftime = state->now; + + // We'll need these for user/group names, so initialize them now to + // avoid EMFILE later + bfs_ctx_users(state->ctx); + bfs_ctx_groups(state->ctx); + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -fprint FILE. + */ +static struct bfs_expr *parse_fprint(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(state, eval_fprint); + if (expr) { + expr_set_always_true(expr); + expr->cost = PRINT_COST; + if (expr_open(state, expr, expr->argv[1]) != 0) { + goto fail; + } + } + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -fprint0 FILE. + */ +static struct bfs_expr *parse_fprint0(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(state, eval_fprint0); + if (expr) { + expr_set_always_true(expr); + expr->cost = PRINT_COST; + if (expr_open(state, expr, expr->argv[1]) != 0) { + goto fail; + } + } + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -fprintf FILE FORMAT. + */ +static struct bfs_expr *parse_fprintf(struct parser_state *state, int arg1, int arg2) { + const char *arg = state->argv[0]; + + const char *file = state->argv[1]; + if (!file) { + parse_error(state, "${blu}%s${rs} needs a file.\n", arg); + return NULL; + } + + const char *format = state->argv[2]; + if (!format) { + parse_error(state, "${blu}%s${rs} needs a format string.\n", arg); + return NULL; + } + + struct bfs_expr *expr = parse_action(state, eval_fprintf, 3); + if (!expr) { + return NULL; + } + + expr_set_always_true(expr); + + expr->cost = PRINT_COST; + + if (expr_open(state, expr, file) != 0) { + goto fail; + } + + if (bfs_printf_parse(state->ctx, expr, format) != 0) { + goto fail; + } + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -fstype TYPE. + */ +static struct bfs_expr *parse_fstype(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_fstype); + if (!expr) { + return NULL; + } + + if (!bfs_ctx_mtab(state->ctx)) { + parse_expr_error(state, expr, "Couldn't parse the mount table: %m.\n"); + bfs_expr_free(expr); + return NULL; + } + + expr->cost = STAT_COST; + return expr; +} + +/** + * Parse -gid/-group. + */ +static struct bfs_expr *parse_group(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_gid); + if (!expr) { + return NULL; + } + + const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); + if (!groups) { + parse_expr_error(state, expr, "Couldn't parse the group table: %m.\n"); + goto fail; + } + + const struct group *grp = bfs_getgrnam(groups, expr->argv[1]); + if (grp) { + expr->num = grp->gr_gid; + expr->int_cmp = BFS_INT_EQUAL; + } else if (looks_like_icmp(expr->argv[1])) { + if (!parse_icmp(state, expr, 0)) { + goto fail; + } + } else { + parse_expr_error(state, expr, "No such group.\n"); + goto fail; + } + + expr->cost = STAT_COST; + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -unique. + */ +static struct bfs_expr *parse_unique(struct parser_state *state, int arg1, int arg2) { + state->ctx->unique = true; + return parse_nullary_option(state); +} + +/** + * Parse -used N. + */ +static struct bfs_expr *parse_used(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_test_icmp(state, eval_used); + if (expr) { + expr->cost = STAT_COST; + } + return expr; +} + +/** + * Parse -uid/-user. + */ +static struct bfs_expr *parse_user(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_uid); + if (!expr) { + return NULL; + } + + const struct bfs_users *users = bfs_ctx_users(state->ctx); + if (!users) { + parse_expr_error(state, expr, "Couldn't parse the user table: %m.\n"); + goto fail; + } + + const struct passwd *pwd = bfs_getpwnam(users, expr->argv[1]); + if (pwd) { + expr->num = pwd->pw_uid; + expr->int_cmp = BFS_INT_EQUAL; + } else if (looks_like_icmp(expr->argv[1])) { + if (!parse_icmp(state, expr, 0)) { + goto fail; + } + } else { + parse_expr_error(state, expr, "No such user.\n"); + goto fail; + } + + expr->cost = STAT_COST; + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -hidden. + */ +static struct bfs_expr *parse_hidden(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_test(state, eval_hidden); + if (expr) { + expr->probability = 0.01; + } + return expr; +} + +/** + * Parse -(no)?ignore_readdir_race. + */ +static struct bfs_expr *parse_ignore_races(struct parser_state *state, int ignore, int arg2) { + state->ctx->ignore_races = ignore; + return parse_nullary_option(state); +} + +/** + * Parse -inum N. + */ +static struct bfs_expr *parse_inum(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_test_icmp(state, eval_inum); + if (expr) { + expr->cost = STAT_COST; + expr->probability = expr->int_cmp == BFS_INT_EQUAL ? 0.01 : 0.50; + } + return expr; +} + +/** + * Parse -links N. + */ +static struct bfs_expr *parse_links(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_test_icmp(state, eval_links); + if (expr) { + expr->cost = STAT_COST; + expr->probability = bfs_expr_cmp(expr, 1) ? 0.99 : 0.01; + } + return expr; +} + +/** + * Parse -ls. + */ +static struct bfs_expr *parse_ls(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(state, eval_fls); + if (!expr) { + return NULL; + } + + init_print_expr(state, expr); + expr->reftime = state->now; + + // We'll need these for user/group names, so initialize them now to + // avoid EMFILE later + bfs_ctx_users(state->ctx); + bfs_ctx_groups(state->ctx); + + return expr; +} + +/** + * Parse -mount. + */ +static struct bfs_expr *parse_mount(struct parser_state *state, int arg1, int arg2) { + parse_warning(state, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", state->argv[0]); + bfs_warning(state->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n"); + + state->ctx->flags |= BFTW_PRUNE_MOUNTS; + state->mount_arg = state->argv; + return parse_nullary_option(state); +} + +/** + * Common code for fnmatch() tests. + */ +static struct bfs_expr *parse_fnmatch(const struct parser_state *state, struct bfs_expr *expr, bool casefold) { + if (!expr) { + return NULL; + } + + if (casefold) { +#ifdef FNM_CASEFOLD + expr->num = FNM_CASEFOLD; +#else + parse_expr_error(state, expr, "Missing platform support.\n"); + bfs_expr_free(expr); + return NULL; +#endif + } else { + expr->num = 0; + } + + // POSIX says, about fnmatch(): + // + // If pattern ends with an unescaped <backslash>, fnmatch() shall + // return a non-zero value (indicating either no match or an error). + // + // But not all implementations obey this, so check for it ourselves. + const char *pattern = expr->argv[1]; + size_t i, len = strlen(pattern); + for (i = 0; i < len; ++i) { + if (pattern[len - i - 1] != '\\') { + break; + } + } + if (i % 2 != 0) { + parse_expr_warning(state, expr, "Unescaped trailing backslash.\n\n"); + bfs_expr_free(expr); + return &bfs_false; + } + + expr->cost = 400.0; + + if (strchr(pattern, '*')) { + expr->probability = 0.5; + } else { + expr->probability = 0.1; + } + + return expr; +} + +/** + * Parse -i?name. + */ +static struct bfs_expr *parse_name(struct parser_state *state, int casefold, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_name); + return parse_fnmatch(state, expr, casefold); +} + +/** + * Parse -i?path, -i?wholename. + */ +static struct bfs_expr *parse_path(struct parser_state *state, int casefold, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_path); + return parse_fnmatch(state, expr, casefold); +} + +/** + * Parse -i?lname. + */ +static struct bfs_expr *parse_lname(struct parser_state *state, int casefold, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_lname); + return parse_fnmatch(state, expr, casefold); +} + +/** Get the bfs_stat_field for X/Y in -newerXY. */ +static enum bfs_stat_field parse_newerxy_field(char c) { + switch (c) { + case 'a': + return BFS_STAT_ATIME; + case 'B': + return BFS_STAT_BTIME; + case 'c': + return BFS_STAT_CTIME; + case 'm': + return BFS_STAT_MTIME; + default: + return 0; + } +} + +/** Parse an explicit reference timestamp for -newerXt and -*since. */ +static int parse_reftime(const struct parser_state *state, struct bfs_expr *expr) { + if (parse_timestamp(expr->argv[1], &expr->reftime) == 0) { + return 0; + } else if (errno != EINVAL) { + parse_expr_error(state, expr, "%m.\n"); + return -1; + } + + parse_expr_error(state, expr, "Invalid timestamp.\n\n"); + fprintf(stderr, "Supported timestamp formats are ISO 8601-like, e.g.\n\n"); + + struct tm tm; + if (xlocaltime(&state->now.tv_sec, &tm) != 0) { + parse_perror(state, "xlocaltime()"); + return -1; + } + + int year = tm.tm_year + 1900; + int month = tm.tm_mon + 1; + fprintf(stderr, " - %04d-%02d-%02d\n", year, month, tm.tm_mday); + fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + +#if __FreeBSD__ + int gmtoff = tm.tm_gmtoff; +#else + int gmtoff = -timezone; +#endif + int tz_hour = gmtoff/3600; + int tz_min = (labs(gmtoff)/60)%60; + fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n", + year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min); + + if (xgmtime(&state->now.tv_sec, &tm) != 0) { + parse_perror(state, "xgmtime()"); + return -1; + } + + year = tm.tm_year + 1900; + month = tm.tm_mon + 1; + fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02dZ\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + + return -1; +} + +/** + * Parse -newerXY. + */ +static struct bfs_expr *parse_newerxy(struct parser_state *state, int arg1, int arg2) { + const char *arg = state->argv[0]; + if (strlen(arg) != 8) { + parse_error(state, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%s${rs}.\n", arg + 6); + return NULL; + } + + struct bfs_expr *expr = parse_unary_test(state, eval_newer); + if (!expr) { + goto fail; + } + + expr->stat_field = parse_newerxy_field(arg[6]); + if (!expr->stat_field) { + parse_expr_error(state, expr, + "For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n", + arg[6]); + goto fail; + } + + if (arg[7] == 't') { + if (parse_reftime(state, expr) != 0) { + goto fail; + } + } else { + enum bfs_stat_field field = parse_newerxy_field(arg[7]); + if (!field) { + parse_expr_error(state, expr, + "For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n", + arg[7]); + goto fail; + } + + struct bfs_stat sb; + if (stat_arg(state, &expr->argv[1], &sb) != 0) { + goto fail; + } + + + const struct timespec *reftime = bfs_stat_time(&sb, field); + if (!reftime) { + parse_expr_error(state, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field)); + goto fail; + } + + expr->reftime = *reftime; + } + + expr->cost = STAT_COST; + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -nogroup. + */ +static struct bfs_expr *parse_nogroup(struct parser_state *state, int arg1, int arg2) { + if (!bfs_ctx_groups(state->ctx)) { + parse_error(state, "Couldn't parse the group table: %m.\n"); + return NULL; + } + + struct bfs_expr *expr = parse_nullary_test(state, eval_nogroup); + if (expr) { + expr->cost = STAT_COST; + expr->probability = 0.01; + } + return expr; +} + +/** + * Parse -nohidden. + */ +static struct bfs_expr *parse_nohidden(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *hidden = bfs_expr_new(eval_hidden, 1, &fake_hidden_arg); + if (!hidden) { + return NULL; + } + + hidden->probability = 0.01; + hidden->pure = true; + hidden->synthetic = true; + + if (parse_exclude(state, hidden) != 0) { + return NULL; + } + + parser_advance(state, T_OPTION, 1); + return &bfs_true; +} + +/** + * Parse -noleaf. + */ +static struct bfs_expr *parse_noleaf(struct parser_state *state, int arg1, int arg2) { + parse_warning(state, "${ex}bfs${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", state->argv[0]); + return parse_nullary_option(state); +} + +/** + * Parse -nouser. + */ +static struct bfs_expr *parse_nouser(struct parser_state *state, int arg1, int arg2) { + if (!bfs_ctx_users(state->ctx)) { + parse_error(state, "Couldn't parse the user table: %m.\n"); + return NULL; + } + + struct bfs_expr *expr = parse_nullary_test(state, eval_nouser); + if (expr) { + expr->cost = STAT_COST; + expr->probability = 0.01; + } + return expr; +} + +/** + * Parse a permission mode like chmod(1). + */ +static int parse_mode(const struct parser_state *state, const char *mode, struct bfs_expr *expr) { + if (mode[0] >= '0' && mode[0] <= '9') { + unsigned int parsed; + if (!parse_int(state, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) { + goto fail; + } + if (parsed > 07777) { + goto fail; + } + + expr->file_mode = parsed; + expr->dir_mode = parsed; + return 0; + } + + expr->file_mode = 0; + expr->dir_mode = 0; + + // Parse the same grammar as chmod(1), which looks like this: + // + // MODE : CLAUSE ["," CLAUSE]* + // + // CLAUSE : WHO* ACTION+ + // + // WHO : "u" | "g" | "o" | "a" + // + // ACTION : OP PERM* + // | OP PERMCOPY + // + // OP : "+" | "-" | "=" + // + // PERM : "r" | "w" | "x" | "X" | "s" | "t" + // + // PERMCOPY : "u" | "g" | "o" + + // State machine state + enum { + MODE_CLAUSE, + MODE_WHO, + MODE_ACTION, + MODE_ACTION_APPLY, + MODE_OP, + MODE_PERM, + } mstate = MODE_CLAUSE; + + enum { + MODE_PLUS, + MODE_MINUS, + MODE_EQUALS, + } op; + + mode_t who; + mode_t file_change; + mode_t dir_change; + + const char *i = mode; + while (true) { + switch (mstate) { + case MODE_CLAUSE: + who = 0; + mstate = MODE_WHO; + BFS_FALLTHROUGH; + + case MODE_WHO: + switch (*i) { + case 'u': + who |= 0700; + break; + case 'g': + who |= 0070; + break; + case 'o': + who |= 0007; + break; + case 'a': + who |= 0777; + break; + default: + mstate = MODE_ACTION; + continue; + } + break; + + case MODE_ACTION_APPLY: + switch (op) { + case MODE_EQUALS: + expr->file_mode &= ~who; + expr->dir_mode &= ~who; + BFS_FALLTHROUGH; + case MODE_PLUS: + expr->file_mode |= file_change; + expr->dir_mode |= dir_change; + break; + case MODE_MINUS: + expr->file_mode &= ~file_change; + expr->dir_mode &= ~dir_change; + break; + } + BFS_FALLTHROUGH; + + case MODE_ACTION: + if (who == 0) { + who = 0777; + } + + switch (*i) { + case '+': + op = MODE_PLUS; + mstate = MODE_OP; + break; + case '-': + op = MODE_MINUS; + mstate = MODE_OP; + break; + case '=': + op = MODE_EQUALS; + mstate = MODE_OP; + break; + + case ',': + if (mstate == MODE_ACTION_APPLY) { + mstate = MODE_CLAUSE; + } else { + goto fail; + } + break; + + case '\0': + if (mstate == MODE_ACTION_APPLY) { + goto done; + } else { + goto fail; + } + + default: + goto fail; + } + break; + + case MODE_OP: + switch (*i) { + case 'u': + file_change = (expr->file_mode >> 6) & 07; + dir_change = (expr->dir_mode >> 6) & 07; + break; + case 'g': + file_change = (expr->file_mode >> 3) & 07; + dir_change = (expr->dir_mode >> 3) & 07; + break; + case 'o': + file_change = expr->file_mode & 07; + dir_change = expr->dir_mode & 07; + break; + + default: + file_change = 0; + dir_change = 0; + mstate = MODE_PERM; + continue; + } + + file_change |= (file_change << 6) | (file_change << 3); + file_change &= who; + dir_change |= (dir_change << 6) | (dir_change << 3); + dir_change &= who; + mstate = MODE_ACTION_APPLY; + break; + + case MODE_PERM: + switch (*i) { + case 'r': + file_change |= who & 0444; + dir_change |= who & 0444; + break; + case 'w': + file_change |= who & 0222; + dir_change |= who & 0222; + break; + case 'x': + file_change |= who & 0111; + BFS_FALLTHROUGH; + case 'X': + dir_change |= who & 0111; + break; + case 's': + if (who & 0700) { + file_change |= S_ISUID; + dir_change |= S_ISUID; + } + if (who & 0070) { + file_change |= S_ISGID; + dir_change |= S_ISGID; + } + break; + case 't': + if (who & 0007) { + file_change |= S_ISVTX; + dir_change |= S_ISVTX; + } + break; + default: + mstate = MODE_ACTION_APPLY; + continue; + } + break; + } + + ++i; + } + +done: + return 0; + +fail: + parse_expr_error(state, expr, "Invalid mode.\n"); + return -1; +} + +/** + * Parse -perm MODE. + */ +static struct bfs_expr *parse_perm(struct parser_state *state, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_perm); + if (!expr) { + return NULL; + } + + const char *mode = expr->argv[1]; + switch (mode[0]) { + case '-': + expr->mode_cmp = BFS_MODE_ALL; + ++mode; + break; + case '/': + expr->mode_cmp = BFS_MODE_ANY; + ++mode; + break; + case '+': + if (mode[1] >= '0' && mode[1] <= '9') { + expr->mode_cmp = BFS_MODE_ANY; + ++mode; + break; + } + BFS_FALLTHROUGH; + default: + expr->mode_cmp = BFS_MODE_EQUAL; + break; + } + + if (parse_mode(state, mode, expr) != 0) { + goto fail; + } + + expr->cost = STAT_COST; + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -print. + */ +static struct bfs_expr *parse_print(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(state, eval_fprint); + if (expr) { + init_print_expr(state, expr); + } + return expr; +} + +/** + * Parse -print0. + */ +static struct bfs_expr *parse_print0(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(state, eval_fprint0); + if (expr) { + init_print_expr(state, expr); + } + return expr; +} + +/** + * Parse -printf FORMAT. + */ +static struct bfs_expr *parse_printf(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(state, eval_fprintf); + if (!expr) { + return NULL; + } + + init_print_expr(state, expr); + + if (bfs_printf_parse(state->ctx, expr, expr->argv[1]) != 0) { + bfs_expr_free(expr); + return NULL; + } + + return expr; +} + +/** + * Parse -printx. + */ +static struct bfs_expr *parse_printx(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(state, eval_fprintx); + if (expr) { + init_print_expr(state, expr); + } + return expr; +} + +/** + * Parse -prune. + */ +static struct bfs_expr *parse_prune(struct parser_state *state, int arg1, int arg2) { + state->prune_arg = state->argv; + + struct bfs_expr *expr = parse_nullary_action(state, eval_prune); + if (expr) { + expr_set_always_true(expr); + } + return expr; +} + +/** + * Parse -quit. + */ +static struct bfs_expr *parse_quit(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(state, eval_quit); + if (expr) { + expr_set_never_returns(expr); + } + return expr; +} + +/** + * Parse -i?regex. + */ +static struct bfs_expr *parse_regex(struct parser_state *state, int flags, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_regex); + if (!expr) { + goto fail; + } + + if (bfs_regcomp(&expr->regex, expr->argv[1], state->regex_type, flags) != 0) { + if (!expr->regex) { + parse_perror(state, "bfs_regcomp()"); + goto fail; + } + + char *str = bfs_regerror(expr->regex); + if (!str) { + parse_perror(state, "bfs_regerror()"); + goto fail; + } + + parse_expr_error(state, expr, "%s.\n", str); + free(str); + goto fail; + } + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -E. + */ +static struct bfs_expr *parse_regex_extended(struct parser_state *state, int arg1, int arg2) { + state->regex_type = BFS_REGEX_POSIX_EXTENDED; + return parse_nullary_flag(state); +} + +/** + * Parse -regextype TYPE. + */ +static struct bfs_expr *parse_regextype(struct parser_state *state, int arg1, int arg2) { + struct bfs_ctx *ctx = state->ctx; + CFILE *cfile = ctx->cerr; + + const char *arg = state->argv[0]; + const char *type = state->argv[1]; + if (!type) { + parse_error(state, "${blu}%s${rs} needs a value.\n\n", arg); + goto list_types; + } + + parser_advance(state, T_OPTION, 1); + + // See https://www.gnu.org/software/gnulib/manual/html_node/Predefined-Syntaxes.html + if (strcmp(type, "posix-basic") == 0 + || strcmp(type, "ed") == 0 + || strcmp(type, "sed") == 0) { + state->regex_type = BFS_REGEX_POSIX_BASIC; + } else if (strcmp(type, "posix-extended") == 0) { + state->regex_type = BFS_REGEX_POSIX_EXTENDED; +#if BFS_WITH_ONIGURUMA + } else if (strcmp(type, "emacs") == 0) { + state->regex_type = BFS_REGEX_EMACS; + } else if (strcmp(type, "grep") == 0) { + state->regex_type = BFS_REGEX_GREP; +#endif + } else if (strcmp(type, "help") == 0) { + state->just_info = true; + cfile = ctx->cout; + goto list_types; + } else { + parse_error(state, "Unsupported regex type.\n\n"); + goto list_types; + } + + parser_advance(state, T_OPTION, 1); + return &bfs_true; + +list_types: + cfprintf(cfile, "Supported types are:\n\n"); + cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); + cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n"); + cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); +#if BFS_WITH_ONIGURUMA + cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n"); + cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); +#endif + cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n"); + return NULL; +} + +/** + * Parse -s. + */ +static struct bfs_expr *parse_s(struct parser_state *state, int arg1, int arg2) { + state->ctx->flags |= BFTW_SORT; + return parse_nullary_flag(state); +} + +/** + * Parse -samefile FILE. + */ +static struct bfs_expr *parse_samefile(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_samefile); + if (!expr) { + return NULL; + } + + struct bfs_stat sb; + if (stat_arg(state, &expr->argv[1], &sb) != 0) { + bfs_expr_free(expr); + return NULL; + } + + expr->dev = sb.dev; + expr->ino = sb.ino; + + expr->cost = STAT_COST; + expr->probability = 0.01; + + return expr; +} + +/** + * Parse -S STRATEGY. + */ +static struct bfs_expr *parse_search_strategy(struct parser_state *state, int arg1, int arg2) { + struct bfs_ctx *ctx = state->ctx; + CFILE *cfile = ctx->cerr; + + const char *flag = state->argv[0]; + const char *arg = state->argv[1]; + if (!arg) { + parse_error(state, "${cyn}%s${rs} needs an argument.\n\n", flag); + goto list_strategies; + } + + parser_advance(state, T_FLAG, 1); + + if (strcmp(arg, "bfs") == 0) { + ctx->strategy = BFTW_BFS; + } else if (strcmp(arg, "dfs") == 0) { + ctx->strategy = BFTW_DFS; + } else if (strcmp(arg, "ids") == 0) { + ctx->strategy = BFTW_IDS; + } else if (strcmp(arg, "eds") == 0) { + ctx->strategy = BFTW_EDS; + } else if (strcmp(arg, "help") == 0) { + state->just_info = true; + cfile = ctx->cout; + goto list_strategies; + } else { + parse_error(state, "Unrecognized search strategy.\n\n"); + goto list_strategies; + } + + parser_advance(state, T_FLAG, 1); + return &bfs_true; + +list_strategies: + cfprintf(cfile, "Supported search strategies:\n\n"); + cfprintf(cfile, " ${bld}bfs${rs}: breadth-first search\n"); + cfprintf(cfile, " ${bld}dfs${rs}: depth-first search\n"); + cfprintf(cfile, " ${bld}ids${rs}: iterative deepening search\n"); + cfprintf(cfile, " ${bld}eds${rs}: exponential deepening search\n"); + return NULL; +} + +/** + * Parse -[aBcm]?since. + */ +static struct bfs_expr *parse_since(struct parser_state *state, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_newer); + if (!expr) { + return NULL; + } + + if (parse_reftime(state, expr) != 0) { + goto fail; + } + + expr->cost = STAT_COST; + expr->stat_field = field; + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -size N[cwbkMGTP]?. + */ +static struct bfs_expr *parse_size(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(state, eval_size); + if (!expr) { + return NULL; + } + + const char *unit = parse_icmp(state, expr, IF_PARTIAL_OK); + if (!unit) { + goto fail; + } + + if (strlen(unit) > 1) { + goto bad_unit; + } + + switch (*unit) { + case '\0': + case 'b': + expr->size_unit = BFS_BLOCKS; + break; + case 'c': + expr->size_unit = BFS_BYTES; + break; + case 'w': + expr->size_unit = BFS_WORDS; + break; + case 'k': + expr->size_unit = BFS_KB; + break; + case 'M': + expr->size_unit = BFS_MB; + break; + case 'G': + expr->size_unit = BFS_GB; + break; + case 'T': + expr->size_unit = BFS_TB; + break; + case 'P': + expr->size_unit = BFS_PB; + break; + + default: + goto bad_unit; + } + + expr->cost = STAT_COST; + expr->probability = expr->int_cmp == BFS_INT_EQUAL ? 0.01 : 0.50; + + return expr; + +bad_unit: + parse_expr_error(state, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%s${rs}.\n", unit); +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -sparse. + */ +static struct bfs_expr *parse_sparse(struct parser_state *state, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_test(state, eval_sparse); + if (expr) { + expr->cost = STAT_COST; + } + return expr; +} + +/** + * Parse -status. + */ +static struct bfs_expr *parse_status(struct parser_state *state, int arg1, int arg2) { + state->ctx->status = true; + return parse_nullary_option(state); +} + +/** + * Parse -x?type [bcdpflsD]. + */ +static struct bfs_expr *parse_type(struct parser_state *state, int x, int arg2) { + bfs_eval_fn *eval = x ? eval_xtype : eval_type; + struct bfs_expr *expr = parse_unary_test(state, eval); + if (!expr) { + return NULL; + } + + unsigned int types = 0; + float probability = 0.0; + + const char *c = expr->argv[1]; + while (true) { + enum bfs_type type; + float type_prob; + + switch (*c) { + case 'b': + type = BFS_BLK; + type_prob = 0.00000721183; + break; + case 'c': + type = BFS_CHR; + type_prob = 0.0000499855; + break; + case 'd': + type = BFS_DIR; + type_prob = 0.114475; + break; + case 'D': + type = BFS_DOOR; + type_prob = 0.000001; + break; + case 'p': + type = BFS_FIFO; + type_prob = 0.00000248684; + break; + case 'f': + type = BFS_REG; + type_prob = 0.859772; + break; + case 'l': + type = BFS_LNK; + type_prob = 0.0256816; + break; + case 's': + type = BFS_SOCK; + type_prob = 0.0000116881; + break; + case 'w': + type = BFS_WHT; + type_prob = 0.000001; + break; + + case '\0': + parse_expr_error(state, expr, "Expected a type flag.\n"); + goto fail; + + default: + parse_expr_error(state, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c); + goto fail; + } + + unsigned int flag = 1 << type; + if (!(types & flag)) { + types |= flag; + probability += type_prob; + } + + ++c; + if (*c == '\0') { + break; + } else if (*c == ',') { + ++c; + continue; + } else { + parse_expr_error(state, expr, "Types must be comma-separated.\n"); + goto fail; + } + } + + expr->num = types; + expr->probability = probability; + + if (x && state->ctx->optlevel < 4) { + // Since -xtype dereferences symbolic links, it may have side + // effects such as reporting permission errors, and thus + // shouldn't be re-ordered without aggressive optimizations + expr->pure = false; + } + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +/** + * Parse -(no)?warn. + */ +static struct bfs_expr *parse_warn(struct parser_state *state, int warn, int arg2) { + state->ctx->warn = warn; + return parse_nullary_option(state); +} + +/** + * Parse -xattr. + */ +static struct bfs_expr *parse_xattr(struct parser_state *state, int arg1, int arg2) { +#if BFS_CAN_CHECK_XATTRS + struct bfs_expr *expr = parse_nullary_test(state, eval_xattr); + if (expr) { + expr->cost = STAT_COST; + expr->probability = 0.01; + } + return expr; +#else + parse_error(state, "Missing platform support.\n"); + return NULL; +#endif +} + +/** + * Parse -xattrname. + */ +static struct bfs_expr *parse_xattrname(struct parser_state *state, int arg1, int arg2) { +#if BFS_CAN_CHECK_XATTRS + struct bfs_expr *expr = parse_unary_test(state, eval_xattrname); + if (expr) { + expr->cost = STAT_COST; + expr->probability = 0.01; + } + return expr; +#else + parse_error(state, "Missing platform support.\n"); + return NULL; +#endif +} + +/** + * Parse -xdev. + */ +static struct bfs_expr *parse_xdev(struct parser_state *state, int arg1, int arg2) { + state->ctx->flags |= BFTW_PRUNE_MOUNTS; + state->xdev_arg = state->argv; + return parse_nullary_option(state); +} + +/** + * Launch a pager for the help output. + */ +static CFILE *launch_pager(pid_t *pid, CFILE *cout) { + char *pager = getenv("PAGER"); + + char *exe; + if (pager && pager[0]) { + exe = bfs_spawn_resolve(pager); + } else { + exe = bfs_spawn_resolve("less"); + if (!exe) { + exe = bfs_spawn_resolve("more"); + } + } + if (!exe) { + goto fail; + } + + int pipefd[2]; + if (pipe(pipefd) != 0) { + goto fail_exe; + } + + FILE *file = fdopen(pipefd[1], "w"); + if (!file) { + goto fail_pipe; + } + pipefd[1] = -1; + + CFILE *ret = cfwrap(file, NULL, true); + if (!ret) { + goto fail_file; + } + file = NULL; + + struct bfs_spawn ctx; + if (bfs_spawn_init(&ctx) != 0) { + goto fail_ret; + } + + if (bfs_spawn_addclose(&ctx, fileno(ret->file)) != 0) { + goto fail_ctx; + } + if (bfs_spawn_adddup2(&ctx, pipefd[0], STDIN_FILENO) != 0) { + goto fail_ctx; + } + if (bfs_spawn_addclose(&ctx, pipefd[0]) != 0) { + goto fail_ctx; + } + + char *argv[] = { + exe, + NULL, + NULL, + }; + + if (strcmp(xbasename(exe), "less") == 0) { + // We know less supports colors, other pagers may not + ret->colors = cout->colors; + argv[1] = "-FKRX"; + } + + *pid = bfs_spawn(exe, &ctx, argv, NULL); + if (*pid < 0) { + goto fail_ctx; + } + + xclose(pipefd[0]); + bfs_spawn_destroy(&ctx); + free(exe); + return ret; + +fail_ctx: + bfs_spawn_destroy(&ctx); +fail_ret: + cfclose(ret); +fail_file: + if (file) { + fclose(file); + } +fail_pipe: + if (pipefd[1] >= 0) { + xclose(pipefd[1]); + } + if (pipefd[0] >= 0) { + xclose(pipefd[0]); + } +fail_exe: + free(exe); +fail: + return cout; +} + +/** + * "Parse" -help. + */ +static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg2) { + CFILE *cout = state->ctx->cout; + + pid_t pager = -1; + if (state->stdout_tty) { + cout = launch_pager(&pager, cout); + } + + cfprintf(cout, "Usage: ${ex}%s${rs} [${cyn}flags${rs}...] [${mag}paths${rs}...] [${blu}expression${rs}...]\n\n", + state->command); + + cfprintf(cout, "${ex}bfs${rs} is compatible with ${ex}find${rs}, with some extensions. " + "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n" + "and ${blu}expressions${rs} may be freely mixed in any order.\n\n"); + + cfprintf(cout, "${bld}Flags:${rs}\n\n"); + + cfprintf(cout, " ${cyn}-H${rs}\n"); + cfprintf(cout, " Follow symbolic links on the command line, but not while searching\n"); + cfprintf(cout, " ${cyn}-L${rs}\n"); + cfprintf(cout, " Follow all symbolic links\n"); + cfprintf(cout, " ${cyn}-P${rs}\n"); + cfprintf(cout, " Never follow symbolic links (the default)\n"); + + cfprintf(cout, " ${cyn}-E${rs}\n"); + cfprintf(cout, " Use extended regular expressions (same as ${blu}-regextype${rs} ${bld}posix-extended${rs})\n"); + cfprintf(cout, " ${cyn}-X${rs}\n"); + cfprintf(cout, " Filter out files with non-${ex}xargs${rs}-safe names\n"); + cfprintf(cout, " ${cyn}-d${rs}\n"); + cfprintf(cout, " Search in post-order (same as ${blu}-depth${rs})\n"); + cfprintf(cout, " ${cyn}-s${rs}\n"); + cfprintf(cout, " Visit directory entries in sorted order\n"); + cfprintf(cout, " ${cyn}-x${rs}\n"); + cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs})\n"); + + cfprintf(cout, " ${cyn}-f${rs} ${mag}PATH${rs}\n"); + cfprintf(cout, " Treat ${mag}PATH${rs} as a path to search (useful if begins with a dash)\n"); + cfprintf(cout, " ${cyn}-D${rs} ${bld}FLAG${rs}\n"); + cfprintf(cout, " Turn on a debugging flag (see ${cyn}-D${rs} ${bld}help${rs})\n"); + cfprintf(cout, " ${cyn}-O${bld}N${rs}\n"); + cfprintf(cout, " Enable optimization level ${bld}N${rs} (default: ${bld}3${rs})\n"); + cfprintf(cout, " ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${rs}|${bld}eds${rs}\n"); + cfprintf(cout, " Use ${bld}b${rs}readth-${bld}f${rs}irst/${bld}d${rs}epth-${bld}f${rs}irst/${bld}i${rs}terative/${bld}e${rs}xponential ${bld}d${rs}eepening ${bld}s${rs}earch\n"); + cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n\n"); + + cfprintf(cout, "${bld}Operators:${rs}\n\n"); + + cfprintf(cout, " ${red}(${rs} ${blu}expression${rs} ${red})${rs}\n\n"); + + cfprintf(cout, " ${red}!${rs} ${blu}expression${rs}\n"); + cfprintf(cout, " ${red}-not${rs} ${blu}expression${rs}\n\n"); + + cfprintf(cout, " ${blu}expression${rs} ${blu}expression${rs}\n"); + cfprintf(cout, " ${blu}expression${rs} ${red}-a${rs} ${blu}expression${rs}\n"); + cfprintf(cout, " ${blu}expression${rs} ${red}-and${rs} ${blu}expression${rs}\n\n"); + + cfprintf(cout, " ${blu}expression${rs} ${red}-o${rs} ${blu}expression${rs}\n"); + cfprintf(cout, " ${blu}expression${rs} ${red}-or${rs} ${blu}expression${rs}\n\n"); + + cfprintf(cout, " ${blu}expression${rs} ${red},${rs} ${blu}expression${rs}\n\n"); + + cfprintf(cout, "${bld}Special forms:${rs}\n\n"); + + cfprintf(cout, " ${red}-exclude${rs} ${blu}expression${rs}\n"); + cfprintf(cout, " Exclude all paths matching the ${blu}expression${rs} from the search.\n\n"); + + cfprintf(cout, "${bld}Options:${rs}\n\n"); + + cfprintf(cout, " ${blu}-color${rs}\n"); + cfprintf(cout, " ${blu}-nocolor${rs}\n"); + cfprintf(cout, " Turn colors on or off (default: ${blu}-color${rs} if outputting to a terminal,\n"); + cfprintf(cout, " ${blu}-nocolor${rs} otherwise)\n"); + cfprintf(cout, " ${blu}-daystart${rs}\n"); + cfprintf(cout, " Measure times relative to the start of today\n"); + cfprintf(cout, " ${blu}-depth${rs}\n"); + cfprintf(cout, " Search in post-order (descendents first)\n"); + cfprintf(cout, " ${blu}-files0-from${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " Search the NUL ('\\0')-separated paths from ${bld}FILE${rs} (${bld}-${rs} for standard input).\n"); + cfprintf(cout, " ${blu}-follow${rs}\n"); + cfprintf(cout, " Follow all symbolic links (same as ${cyn}-L${rs})\n"); + cfprintf(cout, " ${blu}-ignore_readdir_race${rs}\n"); + cfprintf(cout, " ${blu}-noignore_readdir_race${rs}\n"); + cfprintf(cout, " Whether to report an error if ${ex}bfs${rs} detects that the file tree is modified\n"); + cfprintf(cout, " during the search (default: ${blu}-noignore_readdir_race${rs})\n"); + cfprintf(cout, " ${blu}-maxdepth${rs} ${bld}N${rs}\n"); + cfprintf(cout, " ${blu}-mindepth${rs} ${bld}N${rs}\n"); + cfprintf(cout, " Ignore files deeper/shallower than ${bld}N${rs}\n"); + cfprintf(cout, " ${blu}-mount${rs}\n"); + cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs} for now, but will\n"); + cfprintf(cout, " skip mount points entirely in the future)\n"); + cfprintf(cout, " ${blu}-nohidden${rs}\n"); + cfprintf(cout, " Exclude hidden files\n"); + cfprintf(cout, " ${blu}-noleaf${rs}\n"); + cfprintf(cout, " Ignored; for compatibility with GNU find\n"); + cfprintf(cout, " ${blu}-regextype${rs} ${bld}TYPE${rs}\n"); + cfprintf(cout, " Use ${bld}TYPE${rs}-flavored regexes (default: ${bld}posix-basic${rs}; see ${blu}-regextype${rs} ${bld}help${rs})\n"); + cfprintf(cout, " ${blu}-status${rs}\n"); + cfprintf(cout, " Display a status bar while searching\n"); + cfprintf(cout, " ${blu}-unique${rs}\n"); + cfprintf(cout, " Skip any files that have already been seen\n"); + cfprintf(cout, " ${blu}-warn${rs}\n"); + cfprintf(cout, " ${blu}-nowarn${rs}\n"); + cfprintf(cout, " Turn on or off warnings about the command line\n"); + cfprintf(cout, " ${blu}-xdev${rs}\n"); + cfprintf(cout, " Don't descend into other mount points\n\n"); + + cfprintf(cout, "${bld}Tests:${rs}\n\n"); + +#if BFS_CAN_CHECK_ACL + cfprintf(cout, " ${blu}-acl${rs}\n"); + cfprintf(cout, " Find files with a non-trivial Access Control List\n"); +#endif + cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}min${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} minutes ago\n"); + cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}newer${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}FILE${rs} was\n" + " modified\n"); + cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}since${rs} ${bld}TIME${rs}\n"); + cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}TIME${rs}\n"); + cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}time${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} days ago\n"); +#if BFS_CAN_CHECK_CAPABILITIES + cfprintf(cout, " ${blu}-capable${rs}\n"); + cfprintf(cout, " Find files with POSIX.1e capabilities set\n"); +#endif + cfprintf(cout, " ${blu}-depth${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files with depth ${bld}N${rs}\n"); + cfprintf(cout, " ${blu}-empty${rs}\n"); + cfprintf(cout, " Find empty files/directories\n"); + cfprintf(cout, " ${blu}-executable${rs}\n"); + cfprintf(cout, " ${blu}-readable${rs}\n"); + cfprintf(cout, " ${blu}-writable${rs}\n"); + cfprintf(cout, " Find files the current user can execute/read/write\n"); + cfprintf(cout, " ${blu}-false${rs}\n"); + cfprintf(cout, " ${blu}-true${rs}\n"); + cfprintf(cout, " Always false/true\n"); + cfprintf(cout, " ${blu}-fstype${rs} ${bld}TYPE${rs}\n"); + cfprintf(cout, " Find files on file systems with the given ${bld}TYPE${rs}\n"); + cfprintf(cout, " ${blu}-gid${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " ${blu}-uid${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files owned by group/user ID ${bld}N${rs}\n"); + cfprintf(cout, " ${blu}-group${rs} ${bld}NAME${rs}\n"); + cfprintf(cout, " ${blu}-user${rs} ${bld}NAME${rs}\n"); + cfprintf(cout, " Find files owned by the group/user ${bld}NAME${rs}\n"); + cfprintf(cout, " ${blu}-hidden${rs}\n"); + cfprintf(cout, " Find hidden files\n"); +#ifdef FNM_CASEFOLD + cfprintf(cout, " ${blu}-ilname${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-iname${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-ipath${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-iregex${rs} ${bld}REGEX${rs}\n"); + cfprintf(cout, " ${blu}-iwholename${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " Case-insensitive versions of ${blu}-lname${rs}/${blu}-name${rs}/${blu}-path${rs}" + "/${blu}-regex${rs}/${blu}-wholename${rs}\n"); +#endif + cfprintf(cout, " ${blu}-inum${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files with inode number ${bld}N${rs}\n"); + cfprintf(cout, " ${blu}-links${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files with ${bld}N${rs} hard links\n"); + cfprintf(cout, " ${blu}-lname${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " Find symbolic links whose target matches the ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-name${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " Find files whose name matches the ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-newer${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " Find files newer than ${bld}FILE${rs}\n"); + cfprintf(cout, " ${blu}-newer${bld}XY${rs} ${bld}REFERENCE${rs}\n"); + cfprintf(cout, " Find files whose ${bld}X${rs} time is newer than the ${bld}Y${rs} time of" + " ${bld}REFERENCE${rs}. ${bld}X${rs} and ${bld}Y${rs}\n"); + cfprintf(cout, " can be any of [${bld}aBcm${rs}]. ${bld}Y${rs} may also be ${bld}t${rs} to parse ${bld}REFERENCE${rs} an explicit\n"); + cfprintf(cout, " timestamp.\n"); + cfprintf(cout, " ${blu}-nogroup${rs}\n"); + cfprintf(cout, " ${blu}-nouser${rs}\n"); + cfprintf(cout, " Find files owned by nonexistent groups/users\n"); + cfprintf(cout, " ${blu}-path${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-wholename${rs} ${bld}GLOB${rs}\n"); + cfprintf(cout, " Find files whose entire path matches the ${bld}GLOB${rs}\n"); + cfprintf(cout, " ${blu}-perm${rs} ${bld}[-]MODE${rs}\n"); + cfprintf(cout, " Find files with a matching mode\n"); + cfprintf(cout, " ${blu}-regex${rs} ${bld}REGEX${rs}\n"); + cfprintf(cout, " Find files whose entire path matches the regular expression ${bld}REGEX${rs}\n"); + cfprintf(cout, " ${blu}-samefile${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " Find hard links to ${bld}FILE${rs}\n"); + cfprintf(cout, " ${blu}-since${rs} ${bld}TIME${rs}\n"); + cfprintf(cout, " Find files modified since ${bld}TIME${rs}\n"); + cfprintf(cout, " ${blu}-size${rs} ${bld}[-+]N[cwbkMGTP]${rs}\n"); + cfprintf(cout, " Find files with the given size, in 1-byte ${bld}c${rs}haracters, 2-byte ${bld}w${rs}ords,\n"); + cfprintf(cout, " 512-byte ${bld}b${rs}locks (default), or ${bld}k${rs}iB/${bld}M${rs}iB/${bld}G${rs}iB/${bld}T${rs}iB/${bld}P${rs}iB\n"); + cfprintf(cout, " ${blu}-sparse${rs}\n"); + cfprintf(cout, " Find files that occupy fewer disk blocks than expected\n"); + cfprintf(cout, " ${blu}-type${rs} ${bld}[bcdlpfswD]${rs}\n"); + cfprintf(cout, " Find files of the given type\n"); + cfprintf(cout, " ${blu}-used${rs} ${bld}[-+]N${rs}\n"); + cfprintf(cout, " Find files last accessed ${bld}N${rs} days after they were changed\n"); +#if BFS_CAN_CHECK_XATTRS + cfprintf(cout, " ${blu}-xattr${rs}\n"); + cfprintf(cout, " Find files with extended attributes\n"); + cfprintf(cout, " ${blu}-xattrname${rs} ${bld}NAME${rs}\n"); + cfprintf(cout, " Find files with the extended attribute ${bld}NAME${rs}\n"); +#endif + cfprintf(cout, " ${blu}-xtype${rs} ${bld}[bcdlpfswD]${rs}\n"); + cfprintf(cout, " Find files of the given type, following links when ${blu}-type${rs} would not, and\n"); + cfprintf(cout, " vice versa\n\n"); + + cfprintf(cout, "${bld}Actions:${rs}\n\n"); + + cfprintf(cout, " ${blu}-delete${rs}\n"); + cfprintf(cout, " ${blu}-rm${rs}\n"); + cfprintf(cout, " Delete any found files (implies ${blu}-depth${rs})\n"); + cfprintf(cout, " ${blu}-exec${rs} ${bld}command ... {} ;${rs}\n"); + cfprintf(cout, " Execute a command\n"); + cfprintf(cout, " ${blu}-exec${rs} ${bld}command ... {} +${rs}\n"); + cfprintf(cout, " Execute a command with multiple files at once\n"); + cfprintf(cout, " ${blu}-ok${rs} ${bld}command ... {} ;${rs}\n"); + cfprintf(cout, " Prompt the user whether to execute a command\n"); + cfprintf(cout, " ${blu}-execdir${rs} ${bld}command ... {} ;${rs}\n"); + cfprintf(cout, " ${blu}-execdir${rs} ${bld}command ... {} +${rs}\n"); + cfprintf(cout, " ${blu}-okdir${rs} ${bld}command ... {} ;${rs}\n"); + cfprintf(cout, " Like ${blu}-exec${rs}/${blu}-ok${rs}, but run the command in the same directory as the found\n"); + cfprintf(cout, " file(s)\n"); + cfprintf(cout, " ${blu}-exit${rs} [${bld}STATUS${rs}]\n"); + cfprintf(cout, " Exit immediately with the given status (%d if unspecified)\n", EXIT_SUCCESS); + cfprintf(cout, " ${blu}-fls${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " ${blu}-fprint${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " ${blu}-fprint0${rs} ${bld}FILE${rs}\n"); + cfprintf(cout, " ${blu}-fprintf${rs} ${bld}FILE${rs} ${bld}FORMAT${rs}\n"); + cfprintf(cout, " Like ${blu}-ls${rs}/${blu}-print${rs}/${blu}-print0${rs}/${blu}-printf${rs}, but write to ${bld}FILE${rs} instead of standard\n" + " output\n"); + cfprintf(cout, " ${blu}-ls${rs}\n"); + cfprintf(cout, " List files like ${ex}ls${rs} ${bld}-dils${rs}\n"); + cfprintf(cout, " ${blu}-print${rs}\n"); + cfprintf(cout, " Print the path to the found file\n"); + cfprintf(cout, " ${blu}-print0${rs}\n"); + cfprintf(cout, " Like ${blu}-print${rs}, but use the null character ('\\0') as a separator rather than\n"); + cfprintf(cout, " newlines\n"); + cfprintf(cout, " ${blu}-printf${rs} ${bld}FORMAT${rs}\n"); + cfprintf(cout, " Print according to a format string (see ${ex}man${rs} ${bld}find${rs}). The additional format\n"); + cfprintf(cout, " directives %%w and %%W${bld}k${rs} for printing file birth times are supported.\n"); + cfprintf(cout, " ${blu}-printx${rs}\n"); + cfprintf(cout, " Like ${blu}-print${rs}, but escape whitespace and quotation characters, to make the\n"); + cfprintf(cout, " output safe for ${ex}xargs${rs}. Consider using ${blu}-print0${rs} and ${ex}xargs${rs} ${bld}-0${rs} instead.\n"); + cfprintf(cout, " ${blu}-prune${rs}\n"); + cfprintf(cout, " Don't descend into this directory\n"); + cfprintf(cout, " ${blu}-quit${rs}\n"); + cfprintf(cout, " Quit immediately\n"); + cfprintf(cout, " ${blu}-version${rs}\n"); + cfprintf(cout, " Print version information\n"); + cfprintf(cout, " ${blu}-help${rs}\n"); + cfprintf(cout, " Print this help message\n\n"); + + cfprintf(cout, "%s\n", BFS_HOMEPAGE); + + if (pager > 0) { + cfclose(cout); + waitpid(pager, NULL, 0); + } + + state->just_info = true; + return NULL; +} + +/** + * "Parse" -version. + */ +static struct bfs_expr *parse_version(struct parser_state *state, int arg1, int arg2) { + cfprintf(state->ctx->cout, "${ex}bfs${rs} ${bld}%s${rs}\n\n", BFS_VERSION); + + printf("%s\n", BFS_HOMEPAGE); + + state->just_info = true; + return NULL; +} + +typedef struct bfs_expr *parse_fn(struct parser_state *state, int arg1, int arg2); + +/** + * An entry in the parse table for literals. + */ +struct table_entry { + char *arg; + enum token_type type; + parse_fn *parse; + int arg1; + int arg2; + bool prefix; +}; + +/** + * The parse table for literals. + */ +static const struct table_entry parse_table[] = { + {"--", T_FLAG}, + {"--help", T_ACTION, parse_help}, + {"--version", T_ACTION, parse_version}, + {"-Bmin", T_TEST, parse_min, BFS_STAT_BTIME}, + {"-Bnewer", T_TEST, parse_newer, BFS_STAT_BTIME}, + {"-Bsince", T_TEST, parse_since, BFS_STAT_BTIME}, + {"-Btime", T_TEST, parse_time, BFS_STAT_BTIME}, + {"-D", T_FLAG, parse_debug}, + {"-E", T_FLAG, parse_regex_extended}, + {"-H", T_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false}, + {"-L", T_FLAG, parse_follow, BFTW_FOLLOW_ALL, false}, + {"-O", T_FLAG, parse_optlevel, 0, 0, true}, + {"-P", T_FLAG, parse_follow, 0, false}, + {"-S", T_FLAG, parse_search_strategy}, + {"-X", T_FLAG, parse_xargs_safe}, + {"-a", T_OPERATOR}, + {"-acl", T_TEST, parse_acl}, + {"-amin", T_TEST, parse_min, BFS_STAT_ATIME}, + {"-and", T_OPERATOR}, + {"-anewer", T_TEST, parse_newer, BFS_STAT_ATIME}, + {"-asince", T_TEST, parse_since, BFS_STAT_ATIME}, + {"-atime", T_TEST, parse_time, BFS_STAT_ATIME}, + {"-capable", T_TEST, parse_capable}, + {"-cmin", T_TEST, parse_min, BFS_STAT_CTIME}, + {"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME}, + {"-color", T_OPTION, parse_color, true}, + {"-csince", T_TEST, parse_since, BFS_STAT_CTIME}, + {"-ctime", T_TEST, parse_time, BFS_STAT_CTIME}, + {"-d", T_FLAG, parse_depth}, + {"-daystart", T_OPTION, parse_daystart}, + {"-delete", T_ACTION, parse_delete}, + {"-depth", T_OPTION, parse_depth_n}, + {"-empty", T_TEST, parse_empty}, + {"-exclude", T_OPERATOR}, + {"-exec", T_ACTION, parse_exec, 0}, + {"-execdir", T_ACTION, parse_exec, BFS_EXEC_CHDIR}, + {"-executable", T_TEST, parse_access, X_OK}, + {"-exit", T_ACTION, parse_exit}, + {"-f", T_FLAG, parse_f}, + {"-false", T_TEST, parse_const, false}, + {"-files0-from", T_OPTION, parse_files0_from}, + {"-flags", T_TEST, parse_flags}, + {"-fls", T_ACTION, parse_fls}, + {"-follow", T_OPTION, parse_follow, BFTW_FOLLOW_ALL, true}, + {"-fprint", T_ACTION, parse_fprint}, + {"-fprint0", T_ACTION, parse_fprint0}, + {"-fprintf", T_ACTION, parse_fprintf}, + {"-fstype", T_TEST, parse_fstype}, + {"-gid", T_TEST, parse_group}, + {"-group", T_TEST, parse_group}, + {"-help", T_ACTION, parse_help}, + {"-hidden", T_TEST, parse_hidden}, + {"-ignore_readdir_race", T_OPTION, parse_ignore_races, true}, + {"-ilname", T_TEST, parse_lname, true}, + {"-iname", T_TEST, parse_name, true}, + {"-inum", T_TEST, parse_inum}, + {"-ipath", T_TEST, parse_path, true}, + {"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE}, + {"-iwholename", T_TEST, parse_path, true}, + {"-links", T_TEST, parse_links}, + {"-lname", T_TEST, parse_lname, false}, + {"-ls", T_ACTION, parse_ls}, + {"-maxdepth", T_OPTION, parse_depth_limit, false}, + {"-mindepth", T_OPTION, parse_depth_limit, true}, + {"-mmin", T_TEST, parse_min, BFS_STAT_MTIME}, + {"-mnewer", T_TEST, parse_newer, BFS_STAT_MTIME}, + {"-mount", T_OPTION, parse_mount}, + {"-msince", T_TEST, parse_since, BFS_STAT_MTIME}, + {"-mtime", T_TEST, parse_time, BFS_STAT_MTIME}, + {"-name", T_TEST, parse_name, false}, + {"-newer", T_TEST, parse_newer, BFS_STAT_MTIME}, + {"-newer", T_TEST, parse_newerxy, 0, 0, true}, + {"-nocolor", T_OPTION, parse_color, false}, + {"-nogroup", T_TEST, parse_nogroup}, + {"-nohidden", T_TEST, parse_nohidden}, + {"-noignore_readdir_race", T_OPTION, parse_ignore_races, false}, + {"-noleaf", T_OPTION, parse_noleaf}, + {"-not", T_OPERATOR}, + {"-nouser", T_TEST, parse_nouser}, + {"-nowarn", T_OPTION, parse_warn, false}, + {"-o", T_OPERATOR}, + {"-ok", T_ACTION, parse_exec, BFS_EXEC_CONFIRM}, + {"-okdir", T_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR}, + {"-or", T_OPERATOR}, + {"-path", T_TEST, parse_path, false}, + {"-perm", T_TEST, parse_perm}, + {"-print", T_ACTION, parse_print}, + {"-print0", T_ACTION, parse_print0}, + {"-printf", T_ACTION, parse_printf}, + {"-printx", T_ACTION, parse_printx}, + {"-prune", T_ACTION, parse_prune}, + {"-quit", T_ACTION, parse_quit}, + {"-readable", T_TEST, parse_access, R_OK}, + {"-regex", T_TEST, parse_regex, 0}, + {"-regextype", T_OPTION, parse_regextype}, + {"-rm", T_ACTION, parse_delete}, + {"-s", T_FLAG, parse_s}, + {"-samefile", T_TEST, parse_samefile}, + {"-since", T_TEST, parse_since, BFS_STAT_MTIME}, + {"-size", T_TEST, parse_size}, + {"-sparse", T_TEST, parse_sparse}, + {"-status", T_OPTION, parse_status}, + {"-true", T_TEST, parse_const, true}, + {"-type", T_TEST, parse_type, false}, + {"-uid", T_TEST, parse_user}, + {"-unique", T_OPTION, parse_unique}, + {"-used", T_TEST, parse_used}, + {"-user", T_TEST, parse_user}, + {"-version", T_ACTION, parse_version}, + {"-warn", T_OPTION, parse_warn, true}, + {"-wholename", T_TEST, parse_path, false}, + {"-writable", T_TEST, parse_access, W_OK}, + {"-x", T_FLAG, parse_xdev}, + {"-xattr", T_TEST, parse_xattr}, + {"-xattrname", T_TEST, parse_xattrname}, + {"-xdev", T_OPTION, parse_xdev}, + {"-xtype", T_TEST, parse_type, true}, + {0}, +}; + +/** Look up an argument in the parse table. */ +static const struct table_entry *table_lookup(const char *arg) { + for (const struct table_entry *entry = parse_table; entry->arg; ++entry) { + bool match; + if (entry->prefix) { + match = strncmp(arg, entry->arg, strlen(entry->arg)) == 0; + } else { + match = strcmp(arg, entry->arg) == 0; + } + if (match) { + return entry; + } + } + + return NULL; +} + +/** Search for a fuzzy match in the parse table. */ +static const struct table_entry *table_lookup_fuzzy(const char *arg) { + const struct table_entry *best = NULL; + int best_dist; + + for (const struct table_entry *entry = parse_table; entry->arg; ++entry) { + int dist = typo_distance(arg, entry->arg); + if (!best || dist < best_dist) { + best = entry; + best_dist = dist; + } + } + + return best; +} + +/** + * LITERAL : OPTION + * | TEST + * | ACTION + */ +static struct bfs_expr *parse_literal(struct parser_state *state) { + // Paths are already skipped at this point + const char *arg = state->argv[0]; + + if (arg[0] != '-') { + goto unexpected; + } + + const struct table_entry *match = table_lookup(arg); + if (match) { + if (match->parse) { + goto matched; + } else { + goto unexpected; + } + } + + match = table_lookup_fuzzy(arg); + + CFILE *cerr = state->ctx->cerr; + parse_error(state, "Unknown argument; did you mean "); + switch (match->type) { + case T_FLAG: + cfprintf(cerr, "${cyn}%s${rs}?", match->arg); + break; + case T_OPERATOR: + cfprintf(cerr, "${red}%s${rs}?", match->arg); + break; + default: + cfprintf(cerr, "${blu}%s${rs}?", match->arg); + break; + } + + if (!state->interactive || !match->parse) { + fprintf(stderr, "\n"); + goto unmatched; + } + + fprintf(stderr, " "); + if (ynprompt() <= 0) { + goto unmatched; + } + + fprintf(stderr, "\n"); + state->argv[0] = match->arg; + +matched: + return match->parse(state, match->arg1, match->arg2); + +unmatched: + return NULL; + +unexpected: + parse_error(state, "Expected a predicate.\n"); + return NULL; +} + +/** + * FACTOR : "(" EXPR ")" + * | "!" FACTOR | "-not" FACTOR + * | "-exclude" FACTOR + * | LITERAL + */ +static struct bfs_expr *parse_factor(struct parser_state *state) { + if (skip_paths(state) != 0) { + return NULL; + } + + const char *arg = state->argv[0]; + if (!arg) { + parse_argv_error(state, state->last_arg, 1, "Expression terminated prematurely here.\n"); + return NULL; + } + + if (strcmp(arg, "(") == 0) { + parser_advance(state, T_OPERATOR, 1); + + struct bfs_expr *expr = parse_expr(state); + if (!expr) { + return NULL; + } + + if (skip_paths(state) != 0) { + bfs_expr_free(expr); + return NULL; + } + + arg = state->argv[0]; + if (!arg || strcmp(arg, ")") != 0) { + parse_argv_error(state, state->last_arg, 1, "Expected a ${red})${rs}.\n"); + bfs_expr_free(expr); + return NULL; + } + + parser_advance(state, T_OPERATOR, 1); + return expr; + } else if (strcmp(arg, "-exclude") == 0) { + if (state->excluding) { + parse_error(state, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg); + return NULL; + } + + parser_advance(state, T_OPERATOR, 1); + state->excluding = true; + + struct bfs_expr *factor = parse_factor(state); + if (!factor) { + return NULL; + } + + state->excluding = false; + + if (parse_exclude(state, factor) != 0) { + return NULL; + } + + return &bfs_true; + } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) { + char **argv = parser_advance(state, T_OPERATOR, 1); + + struct bfs_expr *factor = parse_factor(state); + if (!factor) { + return NULL; + } + + return new_unary_expr(eval_not, factor, argv); + } else { + return parse_literal(state); + } +} + +/** + * TERM : FACTOR + * | TERM FACTOR + * | TERM "-a" FACTOR + * | TERM "-and" FACTOR + */ +static struct bfs_expr *parse_term(struct parser_state *state) { + struct bfs_expr *term = parse_factor(state); + + while (term) { + if (skip_paths(state) != 0) { + bfs_expr_free(term); + return NULL; + } + + const char *arg = state->argv[0]; + if (!arg) { + break; + } + + if (strcmp(arg, "-o") == 0 || strcmp(arg, "-or") == 0 + || strcmp(arg, ",") == 0 + || strcmp(arg, ")") == 0) { + break; + } + + char **argv = &fake_and_arg; + if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) { + argv = parser_advance(state, T_OPERATOR, 1); + } + + struct bfs_expr *lhs = term; + struct bfs_expr *rhs = parse_factor(state); + if (!rhs) { + bfs_expr_free(lhs); + return NULL; + } + + term = new_binary_expr(eval_and, lhs, rhs, argv); + } + + return term; +} + +/** + * CLAUSE : TERM + * | CLAUSE "-o" TERM + * | CLAUSE "-or" TERM + */ +static struct bfs_expr *parse_clause(struct parser_state *state) { + struct bfs_expr *clause = parse_term(state); + + while (clause) { + if (skip_paths(state) != 0) { + bfs_expr_free(clause); + return NULL; + } + + const char *arg = state->argv[0]; + if (!arg) { + break; + } + + if (strcmp(arg, "-o") != 0 && strcmp(arg, "-or") != 0) { + break; + } + + char **argv = parser_advance(state, T_OPERATOR, 1); + + struct bfs_expr *lhs = clause; + struct bfs_expr *rhs = parse_term(state); + if (!rhs) { + bfs_expr_free(lhs); + return NULL; + } + + clause = new_binary_expr(eval_or, lhs, rhs, argv); + } + + return clause; +} + +/** + * EXPR : CLAUSE + * | EXPR "," CLAUSE + */ +static struct bfs_expr *parse_expr(struct parser_state *state) { + struct bfs_expr *expr = parse_clause(state); + + while (expr) { + if (skip_paths(state) != 0) { + bfs_expr_free(expr); + return NULL; + } + + const char *arg = state->argv[0]; + if (!arg) { + break; + } + + if (strcmp(arg, ",") != 0) { + break; + } + + char **argv = parser_advance(state, T_OPERATOR, 1); + + struct bfs_expr *lhs = expr; + struct bfs_expr *rhs = parse_clause(state); + if (!rhs) { + bfs_expr_free(lhs); + return NULL; + } + + expr = new_binary_expr(eval_comma, lhs, rhs, argv); + } + + return expr; +} + +/** + * Parse the top-level expression. + */ +static struct bfs_expr *parse_whole_expr(struct parser_state *state) { + if (skip_paths(state) != 0) { + return NULL; + } + + struct bfs_expr *expr = &bfs_true; + if (state->argv[0]) { + expr = parse_expr(state); + if (!expr) { + return NULL; + } + } + + if (state->argv[0]) { + parse_error(state, "Unexpected argument.\n"); + goto fail; + } + + if (state->implicit_print) { + struct bfs_expr *print = bfs_expr_new(eval_fprint, 1, &fake_print_arg); + if (!print) { + goto fail; + } + init_print_expr(state, print); + print->synthetic = true; + + expr = new_binary_expr(eval_and, expr, print, &fake_and_arg); + if (!expr) { + goto fail; + } + } + + if (state->mount_arg && state->xdev_arg) { + parse_conflict_warning(state, state->mount_arg, 1, state->xdev_arg, 1, + "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", + state->xdev_arg[0], state->mount_arg[0]); + } + + if (state->ctx->warn && state->depth_arg && state->prune_arg) { + parse_conflict_warning(state, state->depth_arg, 1, state->prune_arg, 1, + "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", + state->prune_arg[0], state->depth_arg[0]); + + if (state->interactive) { + bfs_warning(state->ctx, "Do you want to continue? "); + if (ynprompt() == 0) { + goto fail; + } + } + + fprintf(stderr, "\n"); + } + + if (state->ok_expr && state->files0_stdin_arg) { + parse_conflict_error(state, state->ok_expr->argv, state->ok_expr->argc, state->files0_stdin_arg, 2, + "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n", + state->ok_expr->argv[0], state->files0_stdin_arg[0], state->files0_stdin_arg[1]); + goto fail; + } + + return expr; + +fail: + bfs_expr_free(expr); + return NULL; +} + +void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) { + if (!bfs_debug_prefix(ctx, flag)) { + return; + } + + CFILE *cerr = ctx->cerr; + + cfprintf(cerr, "${ex}%s${rs} ", ctx->argv[0]); + + if (ctx->flags & BFTW_FOLLOW_ALL) { + cfprintf(cerr, "${cyn}-L${rs} "); + } else if (ctx->flags & BFTW_FOLLOW_ROOTS) { + cfprintf(cerr, "${cyn}-H${rs} "); + } else { + cfprintf(cerr, "${cyn}-P${rs} "); + } + + if (ctx->xargs_safe) { + cfprintf(cerr, "${cyn}-X${rs} "); + } + + if (ctx->flags & BFTW_SORT) { + cfprintf(cerr, "${cyn}-s${rs} "); + } + + if (ctx->optlevel != 3) { + cfprintf(cerr, "${cyn}-O${bld}%d${rs} ", ctx->optlevel); + } + + const char *strategy = NULL; + switch (ctx->strategy) { + case BFTW_BFS: + strategy = "bfs"; + break; + case BFTW_DFS: + strategy = "dfs"; + break; + case BFTW_IDS: + strategy = "ids"; + break; + case BFTW_EDS: + strategy = "eds"; + break; + } + assert(strategy); + cfprintf(cerr, "${cyn}-S${rs} ${bld}%s${rs} ", strategy); + + enum debug_flags debug = ctx->debug; + if (debug == DEBUG_ALL) { + cfprintf(cerr, "${cyn}-D${rs} ${bld}all${rs} "); + } else if (debug) { + cfprintf(cerr, "${cyn}-D${rs} "); + for (enum debug_flags i = 1; DEBUG_ALL & i; i <<= 1) { + if (debug & i) { + cfprintf(cerr, "${bld}%s${rs}", debug_flag_name(i)); + debug ^= i; + if (debug) { + cfprintf(cerr, ","); + } + } + } + cfprintf(cerr, " "); + } + + for (size_t i = 0; i < darray_length(ctx->paths); ++i) { + const char *path = ctx->paths[i]; + char c = path[0]; + if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') { + cfprintf(cerr, "${cyn}-f${rs} "); + } + cfprintf(cerr, "${mag}%s${rs} ", path); + } + + if (ctx->cout->colors) { + cfprintf(cerr, "${blu}-color${rs} "); + } else { + cfprintf(cerr, "${blu}-nocolor${rs} "); + } + if (ctx->flags & BFTW_POST_ORDER) { + cfprintf(cerr, "${blu}-depth${rs} "); + } + if (ctx->ignore_races) { + cfprintf(cerr, "${blu}-ignore_readdir_race${rs} "); + } + if (ctx->mindepth != 0) { + cfprintf(cerr, "${blu}-mindepth${rs} ${bld}%d${rs} ", ctx->mindepth); + } + if (ctx->maxdepth != INT_MAX) { + cfprintf(cerr, "${blu}-maxdepth${rs} ${bld}%d${rs} ", ctx->maxdepth); + } + if (ctx->flags & BFTW_SKIP_MOUNTS) { + cfprintf(cerr, "${blu}-mount${rs} "); + } + if (ctx->status) { + cfprintf(cerr, "${blu}-status${rs} "); + } + if (ctx->unique) { + cfprintf(cerr, "${blu}-unique${rs} "); + } + if ((ctx->flags & (BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) == BFTW_PRUNE_MOUNTS) { + cfprintf(cerr, "${blu}-xdev${rs} "); + } + + if (flag == DEBUG_RATES) { + if (ctx->exclude != &bfs_false) { + cfprintf(cerr, "(${red}-exclude${rs} %pE) ", ctx->exclude); + } + cfprintf(cerr, "%pE", ctx->expr); + } else { + if (ctx->exclude != &bfs_false) { + cfprintf(cerr, "(${red}-exclude${rs} %pe) ", ctx->exclude); + } + cfprintf(cerr, "%pe", ctx->expr); + } + + fputs("\n", stderr); +} + +/** + * Dump the estimated costs. + */ +static void dump_costs(const struct bfs_ctx *ctx) { + const struct bfs_expr *expr = ctx->expr; + bfs_debug(ctx, DEBUG_COST, " Cost: ~${ylw}%g${rs}\n", expr->cost); + bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0*expr->probability); +} + +/** + * Get the current time. + */ +static int parse_gettime(const struct bfs_ctx *ctx, struct timespec *ts) { +#if _POSIX_TIMERS > 0 + int ret = clock_gettime(CLOCK_REALTIME, ts); + if (ret != 0) { + bfs_perror(ctx, "clock_gettime()"); + } + return ret; +#else + struct timeval tv; + int ret = gettimeofday(&tv, NULL); + if (ret == 0) { + ts->tv_sec = tv.tv_sec; + ts->tv_nsec = tv.tv_usec * 1000L; + } else { + bfs_perror(ctx, "gettimeofday()"); + } + return ret; +#endif +} + +struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { + struct bfs_ctx *ctx = bfs_ctx_new(); + if (!ctx) { + perror("bfs_new_ctx()"); + goto fail; + } + + static char* default_argv[] = {"bfs", NULL}; + if (argc < 1) { + argc = 1; + argv = default_argv; + } + + ctx->argc = argc; + ctx->argv = malloc((argc + 1)*sizeof(*ctx->argv)); + if (!ctx->argv) { + perror("malloc()"); + goto fail; + } + for (int i = 0; i <= argc; ++i) { + ctx->argv[i] = argv[i]; + } + + enum use_color use_color = COLOR_AUTO; + if (getenv("NO_COLOR")) { + // https://no-color.org/ + use_color = COLOR_NEVER; + } + + ctx->colors = parse_colors(); + if (!ctx->colors) { + ctx->colors_error = errno; + } + + ctx->cerr = cfwrap(stderr, use_color ? ctx->colors : NULL, false); + if (!ctx->cerr) { + perror("cfwrap()"); + goto fail; + } + + ctx->cout = cfwrap(stdout, use_color ? ctx->colors : NULL, false); + if (!ctx->cout) { + bfs_perror(ctx, "cfwrap()"); + goto fail; + } + + if (!bfs_ctx_dedup(ctx, ctx->cout, NULL) || !bfs_ctx_dedup(ctx, ctx->cerr, NULL)) { + bfs_perror(ctx, "bfs_ctx_dedup()"); + goto fail; + } + + bool stdin_tty = isatty(STDIN_FILENO); + bool stdout_tty = isatty(STDOUT_FILENO); + bool stderr_tty = isatty(STDERR_FILENO); + + if (getenv("POSIXLY_CORRECT")) { + ctx->posixly_correct = true; + } else { + ctx->warn = stdin_tty; + } + + struct parser_state state = { + .ctx = ctx, + .argv = ctx->argv + 1, + .command = ctx->argv[0], + .regex_type = BFS_REGEX_POSIX_BASIC, + .stdout_tty = stdout_tty, + .interactive = stdin_tty && stderr_tty, + .use_color = use_color, + .implicit_print = true, + .implicit_root = true, + .just_info = false, + .excluding = false, + .last_arg = NULL, + .depth_arg = NULL, + .prune_arg = NULL, + .mount_arg = NULL, + .xdev_arg = NULL, + .files0_arg = NULL, + .files0_stdin_arg = NULL, + .ok_expr = NULL, + }; + + if (strcmp(xbasename(state.command), "find") == 0) { + // Operate depth-first when invoked as "find" + ctx->strategy = BFTW_DFS; + } + + if (parse_gettime(ctx, &state.now) != 0) { + goto fail; + } + + ctx->exclude = &bfs_false; + ctx->expr = parse_whole_expr(&state); + if (!ctx->expr) { + if (state.just_info) { + goto done; + } else { + goto fail; + } + } + + if (bfs_optimize(ctx) != 0) { + goto fail; + } + + if (darray_length(ctx->paths) == 0) { + if (!state.implicit_root) { + parse_argv_error(&state, state.files0_arg, 2, "No root paths specified.\n"); + goto fail; + } else if (parse_root(&state, ".") != 0) { + goto fail; + } + } + + if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) { + // We need bftw() to detect cycles unless -unique does it for us + ctx->flags |= BFTW_DETECT_CYCLES; + } + + bfs_ctx_dump(ctx, DEBUG_TREE); + dump_costs(ctx); + +done: + return ctx; + +fail: + bfs_ctx_free(ctx); + return NULL; +} diff --git a/src/parse.h b/src/parse.h new file mode 100644 index 0000000..7e29a03 --- /dev/null +++ b/src/parse.h @@ -0,0 +1,36 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020 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. * + ****************************************************************************/ + +/** + * bfs command line parsing. + */ + +#ifndef BFS_PARSE_H +#define BFS_PARSE_H + +/** + * Parse the command line. + * + * @param argc + * The number of arguments. + * @param argv + * The arguments to parse. + * @return + * A new bfs context, or NULL on failure. + */ +struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]); + +#endif // BFS_PARSE_H diff --git a/src/printf.c b/src/printf.c new file mode 100644 index 0000000..8fdde41 --- /dev/null +++ b/src/printf.c @@ -0,0 +1,927 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-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. * + ****************************************************************************/ + +#include "printf.h" +#include "bftw.h" +#include "color.h" +#include "ctx.h" +#include "darray.h" +#include "diag.h" +#include "dir.h" +#include "dstring.h" +#include "expr.h" +#include "mtab.h" +#include "pwcache.h" +#include "stat.h" +#include "util.h" +#include "xtime.h" +#include <assert.h> +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +/** + * A function implementing a printf directive. + */ +typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf); + +/** + * A single printf directive like %f or %#4m. The whole format string is stored + * as a darray of these. + */ +struct bfs_printf { + /** The printing function to invoke. */ + bfs_printf_fn *fn; + /** String data associated with this directive. */ + char *str; + /** The stat field to print. */ + enum bfs_stat_field stat_field; + /** Character data associated with this directive. */ + char c; + /** Some data used by the directive. */ + const void *ptr; +}; + +/** Print some text as-is. */ +static int bfs_printf_literal(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + size_t len = dstrlen(directive->str); + if (fwrite(directive->str, 1, len, cfile->file) == len) { + return 0; + } else { + return -1; + } +} + +/** \c: flush */ +static int bfs_printf_flush(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + return fflush(cfile->file); +} + +/** Check if we can safely colorize this directive. */ +static bool should_color(CFILE *cfile, const struct bfs_printf *directive) { + return cfile->colors && strcmp(directive->str, "%s") == 0; +} + +/** + * Print a value to a temporary buffer before formatting it. + */ +#define BFS_PRINTF_BUF(buf, format, ...) \ + char buf[256]; \ + int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \ + assert(ret >= 0 && (size_t)ret < sizeof(buf)); \ + (void)ret + +/** %a, %c, %t: ctime() */ +static int bfs_printf_ctime(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + // Not using ctime() itself because GNU find adds nanoseconds + static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field); + if (!ts) { + return -1; + } + + struct tm tm; + if (xlocaltime(&ts->tv_sec, &tm) != 0) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d", + days[tm.tm_wday], + months[tm.tm_mon], + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + (long)ts->tv_nsec, + 1900 + tm.tm_year); + + return fprintf(cfile->file, directive->str, buf); +} + +/** %A, %B/%W, %C, %T: strftime() */ +static int bfs_printf_strftime(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field); + if (!ts) { + return -1; + } + + struct tm tm; + if (xlocaltime(&ts->tv_sec, &tm) != 0) { + return -1; + } + + int ret; + char buf[256]; + char format[] = "% "; + switch (directive->c) { + // Non-POSIX strftime() features + case '@': + ret = snprintf(buf, sizeof(buf), "%lld.%09ld0", (long long)ts->tv_sec, (long)ts->tv_nsec); + break; + case '+': + ret = snprintf(buf, sizeof(buf), "%4d-%.2d-%.2d+%.2d:%.2d:%.2d.%09ld0", + 1900 + tm.tm_year, + tm.tm_mon + 1, + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + (long)ts->tv_nsec); + break; + case 'k': + ret = snprintf(buf, sizeof(buf), "%2d", tm.tm_hour); + break; + case 'l': + ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11)%12 + 1); + break; + case 's': + ret = snprintf(buf, sizeof(buf), "%lld", (long long)ts->tv_sec); + break; + case 'S': + ret = snprintf(buf, sizeof(buf), "%.2d.%09ld0", tm.tm_sec, (long)ts->tv_nsec); + break; + case 'T': + ret = snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d.%09ld0", + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + (long)ts->tv_nsec); + break; + + // POSIX strftime() features + default: + format[1] = directive->c; + ret = strftime(buf, sizeof(buf), format, &tm); + break; + } + + assert(ret >= 0 && (size_t)ret < sizeof(buf)); + (void)ret; + + return fprintf(cfile->file, directive->str, buf); +} + +/** %b: blocks */ +static int bfs_printf_b(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 511)/512; + BFS_PRINTF_BUF(buf, "%ju", blocks); + return fprintf(cfile->file, directive->str, buf); +} + +/** %d: depth */ +static int bfs_printf_d(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + return fprintf(cfile->file, directive->str, (intmax_t)ftwbuf->depth); +} + +/** %D: device */ +static int bfs_printf_D(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev); + return fprintf(cfile->file, directive->str, buf); +} + +/** %f: file name */ +static int bfs_printf_f(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + if (should_color(cfile, directive)) { + return cfprintf(cfile, "%pF", ftwbuf); + } else { + return fprintf(cfile->file, directive->str, ftwbuf->path + ftwbuf->nameoff); + } +} + +/** %F: file system type */ +static int bfs_printf_F(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + const char *type = bfs_fstype(directive->ptr, statbuf); + return fprintf(cfile->file, directive->str, type); +} + +/** %G: gid */ +static int bfs_printf_G(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid); + return fprintf(cfile->file, directive->str, buf); +} + +/** %g: group name */ +static int bfs_printf_g(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + const struct bfs_groups *groups = directive->ptr; + const struct group *grp = groups ? bfs_getgrgid(groups, statbuf->gid) : NULL; + if (!grp) { + return bfs_printf_G(cfile, directive, ftwbuf); + } + + return fprintf(cfile->file, directive->str, grp->gr_name); +} + +/** %h: leading directories */ +static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + char *copy = NULL; + const char *buf; + + if (ftwbuf->nameoff > 0) { + size_t len = ftwbuf->nameoff; + if (len > 1) { + --len; + } + + buf = copy = strndup(ftwbuf->path, len); + } else if (ftwbuf->path[0] == '/') { + buf = "/"; + } else { + buf = "."; + } + + if (!buf) { + return -1; + } + + int ret; + if (should_color(cfile, directive)) { + ret = cfprintf(cfile, "${di}%s${rs}", buf); + } else { + ret = fprintf(cfile->file, directive->str, buf); + } + + free(copy); + return ret; +} + +/** %H: current root */ +static int bfs_printf_H(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + if (should_color(cfile, directive)) { + if (ftwbuf->depth == 0) { + return cfprintf(cfile, "%pP", ftwbuf); + } else { + return cfprintf(cfile, "${di}%s${rs}", ftwbuf->root); + } + } else { + return fprintf(cfile->file, directive->str, ftwbuf->root); + } +} + +/** %i: inode */ +static int bfs_printf_i(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino); + return fprintf(cfile->file, directive->str, buf); +} + +/** %k: 1K blocks */ +static int bfs_printf_k(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 1023)/1024; + BFS_PRINTF_BUF(buf, "%ju", blocks); + return fprintf(cfile->file, directive->str, buf); +} + +/** %l: link target */ +static int bfs_printf_l(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + char *buf = NULL; + const char *target = ""; + + if (ftwbuf->type == BFS_LNK) { + if (should_color(cfile, directive)) { + return cfprintf(cfile, "%pL", ftwbuf); + } + + const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW); + size_t len = statbuf ? statbuf->size : 0; + + target = buf = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len); + if (!target) { + return -1; + } + } + + int ret = fprintf(cfile->file, directive->str, target); + free(buf); + return ret; +} + +/** %m: mode */ +static int bfs_printf_m(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + return fprintf(cfile->file, directive->str, (unsigned int)(statbuf->mode & 07777)); +} + +/** %M: symbolic mode */ +static int bfs_printf_M(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + char buf[11]; + xstrmode(statbuf->mode, buf); + return fprintf(cfile->file, directive->str, buf); +} + +/** %n: link count */ +static int bfs_printf_n(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink); + return fprintf(cfile->file, directive->str, buf); +} + +/** %p: full path */ +static int bfs_printf_p(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + if (should_color(cfile, directive)) { + return cfprintf(cfile, "%pP", ftwbuf); + } else { + return fprintf(cfile->file, directive->str, ftwbuf->path); + } +} + +/** %P: path after root */ +static int bfs_printf_P(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + size_t offset = strlen(ftwbuf->root); + if (ftwbuf->path[offset] == '/') { + ++offset; + } + + if (should_color(cfile, directive)) { + if (ftwbuf->depth == 0) { + return 0; + } + + struct BFTW copybuf = *ftwbuf; + copybuf.path += offset; + copybuf.nameoff -= offset; + return cfprintf(cfile, "%pP", ©buf); + } else { + return fprintf(cfile->file, directive->str, ftwbuf->path + offset); + } +} + +/** %s: size */ +static int bfs_printf_s(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size); + return fprintf(cfile->file, directive->str, buf); +} + +/** %S: sparseness */ +static int bfs_printf_S(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + double sparsity; + if (statbuf->size == 0 && statbuf->blocks == 0) { + sparsity = 1.0; + } else { + sparsity = (double)BFS_STAT_BLKSIZE*statbuf->blocks/statbuf->size; + } + return fprintf(cfile->file, directive->str, sparsity); +} + +/** %U: uid */ +static int bfs_printf_U(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid); + return fprintf(cfile->file, directive->str, buf); +} + +/** %u: user name */ +static int bfs_printf_u(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); + if (!statbuf) { + return -1; + } + + const struct bfs_users *users = directive->ptr; + const struct passwd *pwd = users ? bfs_getpwuid(users, statbuf->uid) : NULL; + if (!pwd) { + return bfs_printf_U(cfile, directive, ftwbuf); + } + + return fprintf(cfile->file, directive->str, pwd->pw_name); +} + +static const char *bfs_printf_type(enum bfs_type type) { + switch (type) { + case BFS_BLK: + return "b"; + case BFS_CHR: + return "c"; + case BFS_DIR: + return "d"; + case BFS_DOOR: + return "D"; + case BFS_FIFO: + return "p"; + case BFS_LNK: + return "l"; + case BFS_REG: + return "f"; + case BFS_SOCK: + return "s"; + default: + return "U"; + } +} + +/** %y: type */ +static int bfs_printf_y(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + const char *type = bfs_printf_type(ftwbuf->type); + return fprintf(cfile->file, directive->str, type); +} + +/** %Y: target type */ +static int bfs_printf_Y(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { + int error = 0; + + if (ftwbuf->type != BFS_LNK) { + return bfs_printf_y(cfile, directive, ftwbuf); + } + + const char *type = "U"; + + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_FOLLOW); + if (statbuf) { + type = bfs_printf_type(bfs_mode_to_type(statbuf->mode)); + } else { + switch (errno) { + case ELOOP: + type = "L"; + break; + case ENOENT: + case ENOTDIR: + type = "N"; + break; + default: + type = "?"; + error = errno; + break; + } + } + + int ret = fprintf(cfile->file, directive->str, type); + if (error != 0) { + ret = -1; + errno = error; + } + return ret; +} + +/** + * Append a literal string to the chain. + */ +static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf **format, char **literal) { + if (dstrlen(*literal) == 0) { + return 0; + } + + struct bfs_printf directive = { + .fn = bfs_printf_literal, + .str = *literal, + }; + + if (DARRAY_PUSH(format, &directive) != 0) { + bfs_perror(ctx, "DARRAY_PUSH()"); + return -1; + } + + *literal = dstralloc(0); + if (!*literal) { + bfs_perror(ctx, "dstralloc()"); + return -1; + } + + return 0; +} + +/** + * Append a printf directive to the chain. + */ +static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf **format, char **literal, struct bfs_printf *directive) { + if (append_literal(ctx, format, literal) != 0) { + return -1; + } + + if (DARRAY_PUSH(format, directive) != 0) { + bfs_perror(ctx, "DARRAY_PUSH()"); + return -1; + } + + return 0; +} + +int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format) { + expr->printf = NULL; + + char *literal = dstralloc(0); + if (!literal) { + bfs_perror(ctx, "dstralloc()"); + goto error; + } + + for (const char *i = format; *i; ++i) { + char c = *i; + + if (c == '\\') { + c = *++i; + + if (c >= '0' && c < '8') { + c = 0; + for (int j = 0; j < 3 && *i >= '0' && *i < '8'; ++i, ++j) { + c *= 8; + c += *i - '0'; + } + --i; + goto one_char; + } + + switch (c) { + case 'a': c = '\a'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'v': c = '\v'; break; + case '\\': c = '\\'; break; + + case 'c': + { + struct bfs_printf directive = { + .fn = bfs_printf_flush, + }; + if (append_directive(ctx, &expr->printf, &literal, &directive) != 0) { + goto error; + } + goto done; + } + + case '\0': + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Incomplete escape sequence '\\'.\n"); + goto error; + + default: + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Unrecognized escape sequence '\\%c'.\n", c); + goto error; + } + } else if (c == '%') { + if (i[1] == '%') { + c = *++i; + goto one_char; + } + + struct bfs_printf directive = { + .str = dstralloc(2), + }; + if (!directive.str) { + goto directive_error; + } + if (dstrapp(&directive.str, c) != 0) { + bfs_perror(ctx, "dstrapp()"); + goto directive_error; + } + + const char *specifier = "s"; + + // Parse any flags + bool must_be_numeric = false; + while (true) { + c = *++i; + + switch (c) { + case '#': + case '0': + case '+': + must_be_numeric = true; + BFS_FALLTHROUGH; + case ' ': + case '-': + if (strchr(directive.str, c)) { + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Duplicate flag '%c'.\n", c); + goto directive_error; + } + if (dstrapp(&directive.str, c) != 0) { + bfs_perror(ctx, "dstrapp()"); + goto directive_error; + } + continue; + } + + break; + } + + // Parse the field width + while (c >= '0' && c <= '9') { + if (dstrapp(&directive.str, c) != 0) { + bfs_perror(ctx, "dstrapp()"); + goto directive_error; + } + c = *++i; + } + + // Parse the precision + if (c == '.') { + do { + if (dstrapp(&directive.str, c) != 0) { + bfs_perror(ctx, "dstrapp()"); + goto directive_error; + } + c = *++i; + } while (c >= '0' && c <= '9'); + } + + switch (c) { + case 'a': + directive.fn = bfs_printf_ctime; + directive.stat_field = BFS_STAT_ATIME; + break; + case 'b': + directive.fn = bfs_printf_b; + break; + case 'c': + directive.fn = bfs_printf_ctime; + directive.stat_field = BFS_STAT_CTIME; + break; + case 'd': + directive.fn = bfs_printf_d; + specifier = "jd"; + break; + case 'D': + directive.fn = bfs_printf_D; + break; + case 'f': + directive.fn = bfs_printf_f; + break; + case 'F': + directive.ptr = bfs_ctx_mtab(ctx); + if (!directive.ptr) { + int error = errno; + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Couldn't parse the mount table: %s.\n", strerror(error)); + goto directive_error; + } + directive.fn = bfs_printf_F; + break; + case 'g': + directive.ptr = bfs_ctx_groups(ctx); + if (!directive.ptr) { + int error = errno; + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Couldn't parse the group table: %s.\n", strerror(error)); + goto directive_error; + } + directive.fn = bfs_printf_g; + break; + case 'G': + directive.fn = bfs_printf_G; + break; + case 'h': + directive.fn = bfs_printf_h; + break; + case 'H': + directive.fn = bfs_printf_H; + break; + case 'i': + directive.fn = bfs_printf_i; + break; + case 'k': + directive.fn = bfs_printf_k; + break; + case 'l': + directive.fn = bfs_printf_l; + break; + case 'm': + directive.fn = bfs_printf_m; + specifier = "o"; + break; + case 'M': + directive.fn = bfs_printf_M; + break; + case 'n': + directive.fn = bfs_printf_n; + break; + case 'p': + directive.fn = bfs_printf_p; + break; + case 'P': + directive.fn = bfs_printf_P; + break; + case 's': + directive.fn = bfs_printf_s; + break; + case 'S': + directive.fn = bfs_printf_S; + specifier = "g"; + break; + case 't': + directive.fn = bfs_printf_ctime; + directive.stat_field = BFS_STAT_MTIME; + break; + case 'u': + directive.ptr = bfs_ctx_users(ctx); + if (!directive.ptr) { + int error = errno; + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Couldn't parse the user table: %s.\n", strerror(error)); + goto directive_error; + } + directive.fn = bfs_printf_u; + break; + case 'U': + directive.fn = bfs_printf_U; + break; + case 'w': + directive.fn = bfs_printf_ctime; + directive.stat_field = BFS_STAT_BTIME; + break; + case 'y': + directive.fn = bfs_printf_y; + break; + case 'Y': + directive.fn = bfs_printf_Y; + break; + + case 'A': + directive.stat_field = BFS_STAT_ATIME; + goto directive_strftime; + case 'B': + case 'W': + directive.stat_field = BFS_STAT_BTIME; + goto directive_strftime; + case 'C': + directive.stat_field = BFS_STAT_CTIME; + goto directive_strftime; + case 'T': + directive.stat_field = BFS_STAT_MTIME; + goto directive_strftime; + + directive_strftime: + directive.fn = bfs_printf_strftime; + c = *++i; + if (!c) { + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", directive.str, i[-1]); + goto directive_error; + } else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) { + directive.c = c; + } else { + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Unrecognized time specifier '%%%c%c'.\n", i[-1], c); + goto directive_error; + } + break; + + case '\0': + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Incomplete format specifier '%s'.\n", directive.str); + goto directive_error; + + default: + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Unrecognized format specifier '%%%c'.\n", c); + goto directive_error; + } + + if (must_be_numeric && strcmp(specifier, "s") == 0) { + bfs_expr_error(ctx, expr); + bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", directive.str + 1, c); + goto directive_error; + } + + if (dstrcat(&directive.str, specifier) != 0) { + bfs_perror(ctx, "dstrcat()"); + goto directive_error; + } + + if (append_directive(ctx, &expr->printf, &literal, &directive) != 0) { + goto directive_error; + } + + continue; + + directive_error: + dstrfree(directive.str); + goto error; + } + + one_char: + if (dstrapp(&literal, c) != 0) { + bfs_perror(ctx, "dstrapp()"); + goto error; + } + } + +done: + if (append_literal(ctx, &expr->printf, &literal) != 0) { + goto error; + } + dstrfree(literal); + return 0; + +error: + dstrfree(literal); + bfs_printf_free(expr->printf); + expr->printf = NULL; + return -1; +} + +int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf) { + int ret = 0, error = 0; + + for (size_t i = 0; i < darray_length(format); ++i) { + const struct bfs_printf *directive = &format[i]; + if (directive->fn(cfile, directive, ftwbuf) < 0) { + ret = -1; + error = errno; + } + } + + errno = error; + return ret; +} + +void bfs_printf_free(struct bfs_printf *format) { + for (size_t i = 0; i < darray_length(format); ++i) { + dstrfree(format[i].str); + } + darray_free(format); +} diff --git a/src/printf.h b/src/printf.h new file mode 100644 index 0000000..a8c5f2a --- /dev/null +++ b/src/printf.h @@ -0,0 +1,68 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2017-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 -printf/-fprintf. + */ + +#ifndef BFS_PRINTF_H +#define BFS_PRINTF_H + +#include "color.h" + +struct BFTW; +struct bfs_ctx; +struct bfs_expr; + +/** + * A printf command, the result of parsing a single format string. + */ +struct bfs_printf; + +/** + * Parse a -printf format string. + * + * @param ctx + * The bfs context. + * @param expr + * The expression to fill in. + * @param format + * The format string to parse. + * @return + * 0 on success, -1 on failure. + */ +int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format); + +/** + * Evaluate a parsed format string. + * + * @param cfile + * The CFILE to print to. + * @param format + * The parsed printf format. + * @param ftwbuf + * The bftw() data for the current file. + * @return + * 0 on success, -1 on failure. + */ +int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf); + +/** + * Free a parsed format string. + */ +void bfs_printf_free(struct bfs_printf *format); + +#endif // BFS_PRINTF_H diff --git a/src/pwcache.c b/src/pwcache.c new file mode 100644 index 0000000..91435bd --- /dev/null +++ b/src/pwcache.c @@ -0,0 +1,293 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020 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. * + ****************************************************************************/ + +#include "pwcache.h" +#include "darray.h" +#include "trie.h" +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> + +struct bfs_users { + /** The array of passwd entries. */ + struct passwd *entries; + /** A map from usernames to entries. */ + struct trie by_name; + /** A map from UIDs to entries. */ + struct trie by_uid; +}; + +struct bfs_users *bfs_users_parse(void) { + int error; + + struct bfs_users *users = malloc(sizeof(*users)); + if (!users) { + return NULL; + } + + users->entries = NULL; + trie_init(&users->by_name); + trie_init(&users->by_uid); + + setpwent(); + + while (true) { + errno = 0; + struct passwd *ent = getpwent(); + if (!ent) { + if (errno) { + error = errno; + goto fail_end; + } else { + break; + } + } + + if (DARRAY_PUSH(&users->entries, ent) != 0) { + error = errno; + goto fail_end; + } + + ent = users->entries + darray_length(users->entries) - 1; + ent->pw_name = strdup(ent->pw_name); + ent->pw_dir = strdup(ent->pw_dir); + ent->pw_shell = strdup(ent->pw_shell); + if (!ent->pw_name || !ent->pw_dir || !ent->pw_shell) { + error = ENOMEM; + goto fail_end; + } + } + + endpwent(); + + for (size_t i = 0; i < darray_length(users->entries); ++i) { + struct passwd *entry = &users->entries[i]; + struct trie_leaf *leaf = trie_insert_str(&users->by_name, entry->pw_name); + if (leaf) { + if (!leaf->value) { + leaf->value = entry; + } + } else { + error = errno; + goto fail_free; + } + + leaf = trie_insert_mem(&users->by_uid, &entry->pw_uid, sizeof(entry->pw_uid)); + if (leaf) { + if (!leaf->value) { + leaf->value = entry; + } + } else { + error = errno; + goto fail_free; + } + } + + return users; + +fail_end: + endpwent(); +fail_free: + bfs_users_free(users); + errno = error; + return NULL; +} + +const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name) { + const struct trie_leaf *leaf = trie_find_str(&users->by_name, name); + if (leaf) { + return leaf->value; + } else { + return NULL; + } +} + +const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid) { + const struct trie_leaf *leaf = trie_find_mem(&users->by_uid, &uid, sizeof(uid)); + if (leaf) { + return leaf->value; + } else { + return NULL; + } +} + +void bfs_users_free(struct bfs_users *users) { + if (users) { + trie_destroy(&users->by_uid); + trie_destroy(&users->by_name); + + for (size_t i = 0; i < darray_length(users->entries); ++i) { + struct passwd *entry = &users->entries[i]; + free(entry->pw_shell); + free(entry->pw_dir); + free(entry->pw_name); + } + darray_free(users->entries); + + free(users); + } +} + +struct bfs_groups { + /** The array of group entries. */ + struct group *entries; + /** A map from group names to entries. */ + struct trie by_name; + /** A map from GIDs to entries. */ + struct trie by_gid; +}; + +/** + * struct group::gr_mem isn't properly aligned on macOS, so do this to avoid + * ASAN warnings. + */ +static char *next_gr_mem(void **gr_mem) { + char *mem; + memcpy(&mem, *gr_mem, sizeof(mem)); + *gr_mem = (char *)*gr_mem + sizeof(mem); + return mem; +} + +struct bfs_groups *bfs_groups_parse(void) { + int error; + + struct bfs_groups *groups = malloc(sizeof(*groups)); + if (!groups) { + return NULL; + } + + groups->entries = NULL; + trie_init(&groups->by_name); + trie_init(&groups->by_gid); + + setgrent(); + + while (true) { + errno = 0; + struct group *ent = getgrent(); + if (!ent) { + if (errno) { + error = errno; + goto fail_end; + } else { + break; + } + } + + if (DARRAY_PUSH(&groups->entries, ent) != 0) { + error = errno; + goto fail_end; + } + ent = groups->entries + darray_length(groups->entries) - 1; + + void *members = ent->gr_mem; + ent->gr_mem = NULL; + + ent->gr_name = strdup(ent->gr_name); + if (!ent->gr_name) { + error = errno; + goto fail_end; + } + + for (char *mem = next_gr_mem(&members); mem; mem = next_gr_mem(&members)) { + char *dup = strdup(mem); + if (!dup) { + error = errno; + goto fail_end; + } + + if (DARRAY_PUSH(&ent->gr_mem, &dup) != 0) { + error = errno; + free(dup); + goto fail_end; + } + } + } + + endgrent(); + + for (size_t i = 0; i < darray_length(groups->entries); ++i) { + struct group *entry = &groups->entries[i]; + struct trie_leaf *leaf = trie_insert_str(&groups->by_name, entry->gr_name); + if (leaf) { + if (!leaf->value) { + leaf->value = entry; + } + } else { + error = errno; + goto fail_free; + } + + leaf = trie_insert_mem(&groups->by_gid, &entry->gr_gid, sizeof(entry->gr_gid)); + if (leaf) { + if (!leaf->value) { + leaf->value = entry; + } + } else { + error = errno; + goto fail_free; + } + } + + return groups; + +fail_end: + endgrent(); +fail_free: + bfs_groups_free(groups); + errno = error; + return NULL; +} + +const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name) { + const struct trie_leaf *leaf = trie_find_str(&groups->by_name, name); + if (leaf) { + return leaf->value; + } else { + return NULL; + } +} + +const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid) { + const struct trie_leaf *leaf = trie_find_mem(&groups->by_gid, &gid, sizeof(gid)); + if (leaf) { + return leaf->value; + } else { + return NULL; + } +} + +void bfs_groups_free(struct bfs_groups *groups) { + if (groups) { + trie_destroy(&groups->by_gid); + trie_destroy(&groups->by_name); + + for (size_t i = 0; i < darray_length(groups->entries); ++i) { + struct group *entry = &groups->entries[i]; + for (size_t j = 0; j < darray_length(entry->gr_mem); ++j) { + free(entry->gr_mem[j]); + } + darray_free(entry->gr_mem); + free(entry->gr_name); + } + darray_free(groups->entries); + + free(groups); + } +} diff --git a/src/pwcache.h b/src/pwcache.h new file mode 100644 index 0000000..f1a1db3 --- /dev/null +++ b/src/pwcache.h @@ -0,0 +1,117 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020 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. * + ****************************************************************************/ + +/** + * A caching wrapper for /etc/{passwd,group}. + */ + +#ifndef BFS_PWCACHE_H +#define BFS_PWCACHE_H + +#include <grp.h> +#include <pwd.h> + +/** + * The user table. + */ +struct bfs_users; + +/** + * Parse the user table. + * + * @return + * The parsed user table, or NULL on failure. + */ +struct bfs_users *bfs_users_parse(void); + +/** + * Get a user entry by name. + * + * @param users + * The user table. + * @param name + * The username to look up. + * @return + * The matching user, or NULL if not found. + */ +const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name); + +/** + * Get a user entry by ID. + * + * @param users + * The user table. + * @param uid + * The ID to look up. + * @return + * The matching user, or NULL if not found. + */ +const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid); + +/** + * Free a user table. + * + * @param users + * The user table to free. + */ +void bfs_users_free(struct bfs_users *users); + +/** + * The group table. + */ +struct bfs_groups; + +/** + * Parse the group table. + * + * @return + * The parsed group table, or NULL on failure. + */ +struct bfs_groups *bfs_groups_parse(void); + +/** + * Get a group entry by name. + * + * @param groups + * The group table. + * @param name + * The group name to look up. + * @return + * The matching group, or NULL if not found. + */ +const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name); + +/** + * Get a group entry by ID. + * + * @param groups + * The group table. + * @param uid + * The ID to look up. + * @return + * The matching group, or NULL if not found. + */ +const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid); + +/** + * Free a group table. + * + * @param groups + * The group table to free. + */ +void bfs_groups_free(struct bfs_groups *groups); + +#endif // BFS_PWCACHE_H diff --git a/src/stat.c b/src/stat.c new file mode 100644 index 0000000..47e60b6 --- /dev/null +++ b/src/stat.c @@ -0,0 +1,376 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2018-2019 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. * + ****************************************************************************/ + +#include "stat.h" +#include "util.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> + +#if BFS_HAS_SYS_PARAM +# include <sys/param.h> +#endif + +#ifdef STATX_BASIC_STATS +# define HAVE_STATX true +#elif __linux__ +# include <linux/stat.h> +# include <sys/syscall.h> +# include <unistd.h> +#endif + +#if HAVE_STATX || defined(__NR_statx) +# define HAVE_BFS_STATX true +#endif + +#if __APPLE__ +# define st_atim st_atimespec +# define st_ctim st_ctimespec +# define st_mtim st_mtimespec +# define st_birthtim st_birthtimespec +#endif + +const char *bfs_stat_field_name(enum bfs_stat_field field) { + switch (field) { + case BFS_STAT_DEV: + return "device number"; + case BFS_STAT_INO: + return "inode nunmber"; + case BFS_STAT_TYPE: + return "type"; + case BFS_STAT_MODE: + return "mode"; + case BFS_STAT_NLINK: + return "link count"; + case BFS_STAT_GID: + return "group ID"; + case BFS_STAT_UID: + return "user ID"; + case BFS_STAT_SIZE: + return "size"; + case BFS_STAT_BLOCKS: + return "block count"; + case BFS_STAT_RDEV: + return "underlying device"; + case BFS_STAT_ATTRS: + return "attributes"; + case BFS_STAT_ATIME: + return "access time"; + case BFS_STAT_BTIME: + return "birth time"; + case BFS_STAT_CTIME: + return "change time"; + case BFS_STAT_MTIME: + return "modification time"; + } + + assert(!"Unrecognized stat field"); + return "???"; +} + +/** + * Check if we should retry a failed stat() due to a potentially broken link. + */ +static bool bfs_stat_retry(int ret, enum bfs_stat_flags flags) { + return ret != 0 + && (flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW + && is_nonexistence_error(errno); +} + +/** + * Convert a struct stat to a struct bfs_stat. + */ +static void bfs_stat_convert(const struct stat *statbuf, struct bfs_stat *buf) { + buf->mask = 0; + + buf->dev = statbuf->st_dev; + buf->mask |= BFS_STAT_DEV; + + buf->ino = statbuf->st_ino; + buf->mask |= BFS_STAT_INO; + + buf->mode = statbuf->st_mode; + buf->mask |= BFS_STAT_TYPE | BFS_STAT_MODE; + + buf->nlink = statbuf->st_nlink; + buf->mask |= BFS_STAT_NLINK; + + buf->gid = statbuf->st_gid; + buf->mask |= BFS_STAT_GID; + + buf->uid = statbuf->st_uid; + buf->mask |= BFS_STAT_UID; + + buf->size = statbuf->st_size; + buf->mask |= BFS_STAT_SIZE; + + buf->blocks = statbuf->st_blocks; + buf->mask |= BFS_STAT_BLOCKS; + + buf->rdev = statbuf->st_rdev; + buf->mask |= BFS_STAT_RDEV; + +#if BSD + buf->attrs = statbuf->st_flags; + buf->mask |= BFS_STAT_ATTRS; +#endif + + buf->atime = statbuf->st_atim; + buf->mask |= BFS_STAT_ATIME; + + buf->ctime = statbuf->st_ctim; + buf->mask |= BFS_STAT_CTIME; + + buf->mtime = statbuf->st_mtim; + buf->mask |= BFS_STAT_MTIME; + +#if __APPLE__ || __FreeBSD__ || __NetBSD__ + buf->btime = statbuf->st_birthtim; + buf->mask |= BFS_STAT_BTIME; +#endif +} + +/** + * bfs_stat() implementation backed by stat(). + */ +static int bfs_stat_impl(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags flags, struct bfs_stat *buf) { + struct stat statbuf; + int ret = fstatat(at_fd, at_path, &statbuf, at_flags); + + if (bfs_stat_retry(ret, flags)) { + at_flags |= AT_SYMLINK_NOFOLLOW; + ret = fstatat(at_fd, at_path, &statbuf, at_flags); + } + + if (ret == 0) { + bfs_stat_convert(&statbuf, buf); + } + + return ret; +} + +#if HAVE_BFS_STATX + +/** + * Wrapper for the statx() system call, which had no glibc wrapper prior to 2.28. + */ +static int bfs_statx(int at_fd, const char *at_path, int at_flags, unsigned int mask, struct statx *buf) { +#if BFS_HAS_FEATURE(memory_sanitizer, false) + // -fsanitize=memory doesn't know about statx(), so tell it the memory + // got initialized + memset(buf, 0, sizeof(*buf)); +#endif + +#if HAVE_STATX + return statx(at_fd, at_path, at_flags, mask, buf); +#else + return syscall(__NR_statx, at_fd, at_path, at_flags, mask, buf); +#endif +} + +/** + * bfs_stat() implementation backed by statx(). + */ +static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags flags, struct bfs_stat *buf) { + unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; + struct statx xbuf; + int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf); + + if (bfs_stat_retry(ret, flags)) { + at_flags |= AT_SYMLINK_NOFOLLOW; + ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf); + } + + if (ret != 0) { + return ret; + } + + // Callers shouldn't have to check anything except the times + const unsigned int guaranteed = STATX_BASIC_STATS ^ (STATX_ATIME | STATX_CTIME | STATX_MTIME); + if ((xbuf.stx_mask & guaranteed) != guaranteed) { + errno = ENOTSUP; + return -1; + } + + buf->mask = 0; + + buf->dev = bfs_makedev(xbuf.stx_dev_major, xbuf.stx_dev_minor); + buf->mask |= BFS_STAT_DEV; + + if (xbuf.stx_mask & STATX_INO) { + buf->ino = xbuf.stx_ino; + buf->mask |= BFS_STAT_INO; + } + + buf->mode = xbuf.stx_mode; + if (xbuf.stx_mask & STATX_TYPE) { + buf->mask |= BFS_STAT_TYPE; + } + if (xbuf.stx_mask & STATX_MODE) { + buf->mask |= BFS_STAT_MODE; + } + + if (xbuf.stx_mask & STATX_NLINK) { + buf->nlink = xbuf.stx_nlink; + buf->mask |= BFS_STAT_NLINK; + } + + if (xbuf.stx_mask & STATX_GID) { + buf->gid = xbuf.stx_gid; + buf->mask |= BFS_STAT_GID; + } + + if (xbuf.stx_mask & STATX_UID) { + buf->uid = xbuf.stx_uid; + buf->mask |= BFS_STAT_UID; + } + + if (xbuf.stx_mask & STATX_SIZE) { + buf->size = xbuf.stx_size; + buf->mask |= BFS_STAT_SIZE; + } + + if (xbuf.stx_mask & STATX_BLOCKS) { + buf->blocks = xbuf.stx_blocks; + buf->mask |= BFS_STAT_BLOCKS; + } + + buf->rdev = bfs_makedev(xbuf.stx_rdev_major, xbuf.stx_rdev_minor); + buf->mask |= BFS_STAT_RDEV; + + buf->attrs = xbuf.stx_attributes; + buf->mask |= BFS_STAT_ATTRS; + + if (xbuf.stx_mask & STATX_ATIME) { + buf->atime.tv_sec = xbuf.stx_atime.tv_sec; + buf->atime.tv_nsec = xbuf.stx_atime.tv_nsec; + buf->mask |= BFS_STAT_ATIME; + } + + if (xbuf.stx_mask & STATX_BTIME) { + buf->btime.tv_sec = xbuf.stx_btime.tv_sec; + buf->btime.tv_nsec = xbuf.stx_btime.tv_nsec; + buf->mask |= BFS_STAT_BTIME; + } + + if (xbuf.stx_mask & STATX_CTIME) { + buf->ctime.tv_sec = xbuf.stx_ctime.tv_sec; + buf->ctime.tv_nsec = xbuf.stx_ctime.tv_nsec; + buf->mask |= BFS_STAT_CTIME; + } + + if (xbuf.stx_mask & STATX_MTIME) { + buf->mtime.tv_sec = xbuf.stx_mtime.tv_sec; + buf->mtime.tv_nsec = xbuf.stx_mtime.tv_nsec; + buf->mask |= BFS_STAT_MTIME; + } + + return ret; +} + +#endif // HAVE_BFS_STATX + +/** + * Allows calling stat with custom at_flags. + */ +static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags flags, struct bfs_stat *buf) { +#if HAVE_BFS_STATX + static bool has_statx = true; + + if (has_statx) { + int ret = bfs_statx_impl(at_fd, at_path, at_flags, flags, buf); + // EPERM is commonly returned in a seccomp() sandbox that does + // not allow statx() + if (ret != 0 && (errno == ENOSYS || errno == EPERM)) { + has_statx = false; + } else { + return ret; + } + } +#endif + + return bfs_stat_impl(at_fd, at_path, at_flags, flags, buf); +} + +int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf) { + int at_flags = 0; + if (flags & BFS_STAT_NOFOLLOW) { + at_flags |= AT_SYMLINK_NOFOLLOW; + } + +#ifdef AT_STATX_DONT_SYNC + if (flags & BFS_STAT_NOSYNC) { + at_flags |= AT_STATX_DONT_SYNC; + } +#endif + + if (at_path) { + return bfs_stat_explicit(at_fd, at_path, at_flags, flags, buf); + } + + // Check __GNU__ to work around https://lists.gnu.org/archive/html/bug-hurd/2021-12/msg00001.html +#if defined(AT_EMPTY_PATH) && !__GNU__ + static bool has_at_ep = true; + if (has_at_ep) { + at_flags |= AT_EMPTY_PATH; + int ret = bfs_stat_explicit(at_fd, "", at_flags, flags, buf); + if (ret != 0 && errno == EINVAL) { + has_at_ep = false; + } else { + return ret; + } + } +#endif + + struct stat statbuf; + if (fstat(at_fd, &statbuf) == 0) { + bfs_stat_convert(&statbuf, buf); + return 0; + } else { + return -1; + } +} + +const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field) { + if (!(buf->mask & field)) { + errno = ENOTSUP; + return NULL; + } + + switch (field) { + case BFS_STAT_ATIME: + return &buf->atime; + case BFS_STAT_BTIME: + return &buf->btime; + case BFS_STAT_CTIME: + return &buf->ctime; + case BFS_STAT_MTIME: + return &buf->mtime; + default: + assert(!"Invalid stat field for time"); + errno = EINVAL; + return NULL; + } +} + +void bfs_stat_id(const struct bfs_stat *buf, bfs_file_id *id) { + memcpy(*id, &buf->dev, sizeof(buf->dev)); + memcpy(*id + sizeof(buf->dev), &buf->ino, sizeof(buf->ino)); +} diff --git a/src/stat.h b/src/stat.h new file mode 100644 index 0000000..55c75e9 --- /dev/null +++ b/src/stat.h @@ -0,0 +1,155 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2018-2019 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. * + ****************************************************************************/ + +/** + * A facade over the stat() API that unifies some details that diverge between + * implementations, like the names of the timespec fields and the presence of + * file "birth" times. On new enough Linux kernels, the facade is backed by + * statx() instead, and so it exposes a similar interface with a mask for which + * fields were successfully returned. + */ + +#ifndef BFS_STAT_H +#define BFS_STAT_H + +#include "util.h" +#include <sys/types.h> +#include <time.h> + +#if BFS_HAS_SYS_PARAM +# include <sys/param.h> +#endif + +/** + * bfs_stat field bitmask. + */ +enum bfs_stat_field { + BFS_STAT_DEV = 1 << 0, + BFS_STAT_INO = 1 << 1, + BFS_STAT_TYPE = 1 << 2, + BFS_STAT_MODE = 1 << 3, + BFS_STAT_NLINK = 1 << 4, + BFS_STAT_GID = 1 << 5, + BFS_STAT_UID = 1 << 6, + BFS_STAT_SIZE = 1 << 7, + BFS_STAT_BLOCKS = 1 << 8, + BFS_STAT_RDEV = 1 << 9, + BFS_STAT_ATTRS = 1 << 10, + BFS_STAT_ATIME = 1 << 11, + BFS_STAT_BTIME = 1 << 12, + BFS_STAT_CTIME = 1 << 13, + BFS_STAT_MTIME = 1 << 14, +}; + +/** + * Get the human-readable name of a bfs_stat field. + */ +const char *bfs_stat_field_name(enum bfs_stat_field field); + +/** + * bfs_stat() flags. + */ +enum bfs_stat_flags { + /** Follow symlinks (the default). */ + BFS_STAT_FOLLOW = 0, + /** Never follow symlinks. */ + BFS_STAT_NOFOLLOW = 1 << 0, + /** Try to follow symlinks, but fall back to the link itself if broken. */ + BFS_STAT_TRYFOLLOW = 1 << 1, + /** Try to use cached values without synchronizing remote filesystems. */ + BFS_STAT_NOSYNC = 1 << 2, +}; + +#ifdef DEV_BSIZE +# define BFS_STAT_BLKSIZE DEV_BSIZE +#elif defined(S_BLKSIZE) +# define BFS_STAT_BLKSIZE S_BLKSIZE +#else +# define BFS_STAT_BLKSIZE 512 +#endif + +/** + * Facade over struct stat. + */ +struct bfs_stat { + /** Bitmask indicating filled fields. */ + enum bfs_stat_field mask; + + /** Device ID containing the file. */ + dev_t dev; + /** Inode number. */ + ino_t ino; + /** File type and access mode. */ + mode_t mode; + /** Number of hard links. */ + nlink_t nlink; + /** Owner group ID. */ + gid_t gid; + /** Owner user ID. */ + uid_t uid; + /** File size in bytes. */ + off_t size; + /** Number of disk blocks allocated (of size BFS_STAT_BLKSIZE). */ + blkcnt_t blocks; + /** The device ID represented by this file. */ + dev_t rdev; + + /** Attributes/flags set on the file. */ + unsigned long long attrs; + + /** Access time. */ + struct timespec atime; + /** Birth/creation time. */ + struct timespec btime; + /** Status change time. */ + struct timespec ctime; + /** Modification time. */ + struct timespec mtime; +}; + +/** + * Facade over fstatat(). + * + * @param at_fd + * The base file descriptor for the lookup. + * @param at_path + * The path to stat, relative to at_fd. Pass NULL to fstat() at_fd + * itself. + * @param flags + * Flags that affect the lookup. + * @param[out] buf + * A place to store the stat buffer, if successful. + * @return + * 0 on success, -1 on error. + */ +int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf); + +/** + * Get a particular time field from a bfs_stat() buffer. + */ +const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field); + +/** + * A unique ID for a file. + */ +typedef unsigned char bfs_file_id[sizeof(dev_t) + sizeof(ino_t)]; + +/** + * Compute a unique ID for a file. + */ +void bfs_stat_id(const struct bfs_stat *buf, bfs_file_id *id); + +#endif // BFS_STAT_H diff --git a/src/trie.c b/src/trie.c new file mode 100644 index 0000000..bae9acb --- /dev/null +++ b/src/trie.c @@ -0,0 +1,693 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019 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. * + ****************************************************************************/ + +/** + * This is an implementation of a "qp trie," as documented at + * https://dotat.at/prog/qp/README.html + * + * An uncompressed trie over the dataset {AAAA, AADD, ABCD, DDAA, DDDD} would + * look like + * + * A A A A + * *--->*--->*--->*--->$ + * | | | D D + * | | +--->*--->$ + * | | B C D + * | +--->*--->*--->$ + * | D D A A + * +--->*--->*--->*--->$ + * | D D + * +--->*--->$ + * + * A compressed (PATRICIA) trie collapses internal nodes that have only a single + * child, like this: + * + * A A AA + * *--->*--->*---->$ + * | | | DD + * | | +---->$ + * | | BCD + * | +----->$ + * | DD AA + * +---->*---->$ + * | DD + * +---->$ + * + * The nodes can be compressed further by dropping the actual compressed + * sequences from the nodes, storing it only in the leaves. This is the + * technique applied in QP tries, and the crit-bit trees that inspired them + * (https://cr.yp.to/critbit.html). Only the index to test, and the values to + * branch on, need to be stored in each node. + * + * A A A + * 0--->1--->2--->AAAA + * | | | D + * | | +--->AADD + * | | B + * | +--->ABCD + * | D A + * +--->2--->DDAA + * | D + * +--->DDDD + * + * Nodes are represented very compactly. Rather than a dense array of children, + * a sparse array of only the non-NULL children directly follows the node in + * memory. A bitmap is used to track which children exist; the index of a child + * i is found by counting the number of bits below bit i that are set. A tag + * bit is used to tell pointers to internal nodes apart from pointers to leaves. + * + * This implementation tests a whole nibble (half byte/hex digit) at every + * branch, so the bitmap takes up 16 bits. The remainder of a machine word is + * used to hold the offset, which severely constrains its range on 32-bit + * platforms. As a workaround, we store relative instead of absolute offsets, + * and insert intermediate singleton "jump" nodes when necessary. + */ + +#include "trie.h" +#include "util.h" +#include <assert.h> +#include <limits.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#if CHAR_BIT != 8 +# error "This trie implementation assumes 8-bit bytes." +#endif + +/** Number of bits for the sparse array bitmap, aka the range of a nibble. */ +#define BITMAP_BITS 16 +/** The number of remaining bits in a word, to hold the offset. */ +#define OFFSET_BITS (sizeof(size_t)*CHAR_BIT - BITMAP_BITS) +/** The highest representable offset (only 64k on a 32-bit architecture). */ +#define OFFSET_MAX (((size_t)1 << OFFSET_BITS) - 1) + +/** + * An internal node of the trie. + */ +struct trie_node { + /** + * A bitmap that hold which indices exist in the sparse children array. + * Bit i will be set if a child exists at logical index i, and its index + * into the array will be popcount(bitmap & ((1 << i) - 1)). + */ + size_t bitmap : BITMAP_BITS; + + /** + * The offset into the key in nibbles. This is relative to the parent + * node, to support offsets larger than OFFSET_MAX. + */ + size_t offset : OFFSET_BITS; + + /** + * Flexible array of children. Each pointer uses the lowest bit as a + * tag to distinguish internal nodes from leaves. This is safe as long + * as all dynamic allocations are aligned to more than a single byte. + */ + uintptr_t children[]; +}; + +/** Check if an encoded pointer is to a leaf. */ +static bool trie_is_leaf(uintptr_t ptr) { + return ptr & 1; +} + +/** Decode a pointer to a leaf. */ +static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) { + assert(trie_is_leaf(ptr)); + return (struct trie_leaf *)(ptr ^ 1); +} + +/** Encode a pointer to a leaf. */ +static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) { + uintptr_t ptr = (uintptr_t)leaf ^ 1; + assert(trie_is_leaf(ptr)); + return ptr; +} + +/** Decode a pointer to an internal node. */ +static struct trie_node *trie_decode_node(uintptr_t ptr) { + assert(!trie_is_leaf(ptr)); + return (struct trie_node *)ptr; +} + +/** Encode a pointer to an internal node. */ +static uintptr_t trie_encode_node(const struct trie_node *node) { + uintptr_t ptr = (uintptr_t)node; + assert(!trie_is_leaf(ptr)); + return ptr; +} + +void trie_init(struct trie *trie) { + trie->root = 0; +} + +/** Compute the popcount (Hamming weight) of a bitmap. */ +static unsigned int trie_popcount(unsigned int n) { +#if __POPCNT__ + // Use the x86 instruction if we have it. Otherwise, GCC generates a + // library call, so use the below implementation instead. + return __builtin_popcount(n); +#else + // See https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation + n -= (n >> 1) & 0x5555; + n = (n & 0x3333) + ((n >> 2) & 0x3333); + n = (n + (n >> 4)) & 0x0F0F; + n = (n + (n >> 8)) & 0xFF; + return n; +#endif +} + +/** Extract the nibble at a certain offset from a byte sequence. */ +static unsigned char trie_key_nibble(const void *key, size_t offset) { + const unsigned char *bytes = key; + size_t byte = offset >> 1; + + // A branchless version of + // if (offset & 1) { + // return bytes[byte] >> 4; + // } else { + // return bytes[byte] & 0xF; + // } + unsigned int shift = (offset & 1) << 2; + return (bytes[byte] >> shift) & 0xF; +} + +/** + * Finds a leaf in the trie that matches the key at every branch. If the key + * exists in the trie, the representative will match the searched key. But + * since only branch points are tested, it can be different from the key. In + * that case, the first mismatch between the key and the representative will be + * the depth at which to make a new branch to insert the key. + */ +static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) { + uintptr_t ptr = trie->root; + if (!ptr) { + return NULL; + } + + size_t offset = 0; + while (!trie_is_leaf(ptr)) { + struct trie_node *node = trie_decode_node(ptr); + offset += node->offset; + + unsigned int index = 0; + if ((offset >> 1) < length) { + unsigned char nibble = trie_key_nibble(key, offset); + unsigned int bit = 1U << nibble; + if (node->bitmap & bit) { + index = trie_popcount(node->bitmap & (bit - 1)); + } + } + ptr = node->children[index]; + } + + return trie_decode_leaf(ptr); +} + +struct trie_leaf *trie_first_leaf(const struct trie *trie) { + return trie_representative(trie, NULL, 0); +} + +struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) { + return trie_find_mem(trie, key, strlen(key) + 1); +} + +struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) { + struct trie_leaf *rep = trie_representative(trie, key, length); + if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) { + return rep; + } else { + return NULL; + } +} + +struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) { + size_t length = strlen(key); + struct trie_leaf *rep = trie_representative(trie, key, length + 1); + if (rep && rep->length >= length && memcmp(rep->key, key, length) == 0) { + return rep; + } else { + return NULL; + } +} + +/** + * Find a leaf that may end at the current node. + */ +static struct trie_leaf *trie_terminal_leaf(const struct trie_node *node) { + // Finding a terminating NUL byte may take two nibbles + for (int i = 0; i < 2; ++i) { + if (!(node->bitmap & 1)) { + break; + } + + uintptr_t ptr = node->children[0]; + if (trie_is_leaf(ptr)) { + return trie_decode_leaf(ptr); + } else { + node = trie_decode_node(ptr); + } + } + + return NULL; +} + +/** Check if a leaf is a prefix of a search key. */ +static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *key, size_t length) { + if (leaf && leaf->length <= length) { + return memcmp(key + skip, leaf->key + skip, leaf->length - skip - 1) == 0; + } else { + return false; + } +} + +struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) { + uintptr_t ptr = trie->root; + if (!ptr) { + return NULL; + } + + struct trie_leaf *best = NULL; + size_t skip = 0; + size_t length = strlen(key) + 1; + + size_t offset = 0; + while (!trie_is_leaf(ptr)) { + struct trie_node *node = trie_decode_node(ptr); + offset += node->offset; + if ((offset >> 1) >= length) { + return best; + } + + struct trie_leaf *leaf = trie_terminal_leaf(node); + if (trie_check_prefix(leaf, skip, key, length)) { + best = leaf; + skip = offset >> 1; + } + + unsigned char nibble = trie_key_nibble(key, offset); + unsigned int bit = 1U << nibble; + if (node->bitmap & bit) { + unsigned int index = trie_popcount(node->bitmap & (bit - 1)); + ptr = node->children[index]; + } else { + return best; + } + } + + struct trie_leaf *leaf = trie_decode_leaf(ptr); + if (trie_check_prefix(leaf, skip, key, length)) { + best = leaf; + } + + return best; +} + +/** Create a new leaf, holding a copy of the given key. */ +static struct trie_leaf *new_trie_leaf(const void *key, size_t length) { + struct trie_leaf *leaf = malloc(BFS_FLEX_SIZEOF(struct trie_leaf, key, length)); + if (leaf) { + leaf->value = NULL; + leaf->length = length; + memcpy(leaf->key, key, length); + } + return leaf; +} + +/** Compute the size of a trie node with a certain number of children. */ +static size_t trie_node_size(unsigned int size) { + // Empty nodes aren't supported + assert(size > 0); + // Node size must be a power of two + assert((size & (size - 1)) == 0); + + return BFS_FLEX_SIZEOF(struct trie_node, children, size); +} + +/** Find the offset of the first nibble that differs between two keys. */ +static size_t trie_key_mismatch(const void *key1, const void *key2, size_t length) { + const unsigned char *bytes1 = key1; + const unsigned char *bytes2 = key2; + size_t i = 0; + size_t offset = 0; + const size_t chunk = sizeof(size_t); + + for (; i + chunk <= length; i += chunk) { + if (memcmp(bytes1 + i, bytes2 + i, chunk) != 0) { + break; + } + } + + for (; i < length; ++i) { + unsigned char b1 = bytes1[i], b2 = bytes2[i]; + if (b1 != b2) { + offset = (b1 & 0xF) == (b2 & 0xF); + break; + } + } + + offset |= i << 1; + return offset; +} + +/** + * Insert a key into a node. The node must not have a child in that position + * already. Effectively takes a subtrie like this: + * + * ptr + * | + * v X + * *--->... + * | Z + * +--->... + * + * and transforms it to: + * + * ptr + * | + * v X + * *--->... + * | Y + * +--->key + * | Z + * +--->... + */ +static struct trie_leaf *trie_node_insert(uintptr_t *ptr, const void *key, size_t length, size_t offset) { + struct trie_node *node = trie_decode_node(*ptr); + unsigned int size = trie_popcount(node->bitmap); + + // Double the capacity every power of two + if ((size & (size - 1)) == 0) { + node = realloc(node, trie_node_size(2*size)); + if (!node) { + return NULL; + } + *ptr = trie_encode_node(node); + } + + struct trie_leaf *leaf = new_trie_leaf(key, length); + if (!leaf) { + return NULL; + } + + unsigned char nibble = trie_key_nibble(key, offset); + unsigned int bit = 1U << nibble; + + // The child must not already be present + assert(!(node->bitmap & bit)); + node->bitmap |= bit; + + unsigned int index = trie_popcount(node->bitmap & (bit - 1)); + uintptr_t *child = &node->children[index]; + if (index < size) { + memmove(child + 1, child, (size - index)*sizeof(*child)); + } + *child = trie_encode_leaf(leaf); + return leaf; +} + +/** + * When the current offset exceeds OFFSET_MAX, insert "jump" nodes that bridge + * the gap. This function takes a subtrie like this: + * + * ptr + * | + * v + * *--->rep + * + * and changes it to: + * + * ptr ret + * | | + * v v + * *--->*--->rep + * + * so that a new key can be inserted like: + * + * ptr ret + * | | + * v v X + * *--->*--->rep + * | Y + * +--->key + */ +static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) { + // We only ever need to jump to leaf nodes, since internal nodes are + // guaranteed to be within OFFSET_MAX anyway + assert(trie_is_leaf(*ptr)); + + struct trie_node *node = malloc(trie_node_size(1)); + if (!node) { + return NULL; + } + + *offset += OFFSET_MAX; + node->offset = OFFSET_MAX; + + unsigned char nibble = trie_key_nibble(key, *offset); + node->bitmap = 1 << nibble; + + node->children[0] = *ptr; + *ptr = trie_encode_node(node); + return node->children; +} + +/** + * Split a node in the trie. Changes a subtrie like this: + * + * ptr + * | + * v + * *...>--->rep + * + * into this: + * + * ptr + * | + * v X + * *--->*...>--->rep + * | Y + * +--->key + */ +static struct trie_leaf *trie_split(uintptr_t *ptr, const void *key, size_t length, struct trie_leaf *rep, size_t offset, size_t mismatch) { + unsigned char key_nibble = trie_key_nibble(key, mismatch); + unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch); + assert(key_nibble != rep_nibble); + + struct trie_node *node = malloc(trie_node_size(2)); + if (!node) { + return NULL; + } + + struct trie_leaf *leaf = new_trie_leaf(key, length); + if (!leaf) { + free(node); + return NULL; + } + + node->bitmap = (1 << key_nibble) | (1 << rep_nibble); + + size_t delta = mismatch - offset; + if (!trie_is_leaf(*ptr)) { + struct trie_node *child = trie_decode_node(*ptr); + child->offset -= delta; + } + node->offset = delta; + + unsigned int key_index = key_nibble > rep_nibble; + node->children[key_index] = trie_encode_leaf(leaf); + node->children[key_index ^ 1] = *ptr; + *ptr = trie_encode_node(node); + return leaf; +} + +struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) { + return trie_insert_mem(trie, key, strlen(key) + 1); +} + +struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) { + struct trie_leaf *rep = trie_representative(trie, key, length); + if (!rep) { + struct trie_leaf *leaf = new_trie_leaf(key, length); + if (leaf) { + trie->root = trie_encode_leaf(leaf); + } + return leaf; + } + + size_t limit = length < rep->length ? length : rep->length; + size_t mismatch = trie_key_mismatch(key, rep->key, limit); + if ((mismatch >> 1) >= length) { + return rep; + } + + size_t offset = 0; + uintptr_t *ptr = &trie->root; + while (!trie_is_leaf(*ptr)) { + struct trie_node *node = trie_decode_node(*ptr); + if (offset + node->offset > mismatch) { + break; + } + offset += node->offset; + + unsigned char nibble = trie_key_nibble(key, offset); + unsigned int bit = 1U << nibble; + if (node->bitmap & bit) { + assert(offset < mismatch); + unsigned int index = trie_popcount(node->bitmap & (bit - 1)); + ptr = &node->children[index]; + } else { + assert(offset == mismatch); + return trie_node_insert(ptr, key, length, offset); + } + } + + while (mismatch - offset > OFFSET_MAX) { + ptr = trie_jump(ptr, key, &offset); + if (!ptr) { + return NULL; + } + } + + return trie_split(ptr, key, length, rep, offset, mismatch); +} + +/** Free a chain of singleton nodes. */ +static void trie_free_singletons(uintptr_t ptr) { + while (!trie_is_leaf(ptr)) { + struct trie_node *node = trie_decode_node(ptr); + + // Make sure the bitmap is a power of two, i.e. it has just one child + assert((node->bitmap & (node->bitmap - 1)) == 0); + + ptr = node->children[0]; + free(node); + } + + free(trie_decode_leaf(ptr)); +} + +/** + * Try to collapse a two-child node like: + * + * parent child + * | | + * v v + * *----->*----->*----->leaf + * | + * +----->other + * + * into + * + * parent + * | + * v + * other + */ +static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) { + uintptr_t other = parent_node->children[child_index ^ 1]; + if (!trie_is_leaf(other)) { + struct trie_node *other_node = trie_decode_node(other); + if (other_node->offset + parent_node->offset <= OFFSET_MAX) { + other_node->offset += parent_node->offset; + } else { + return -1; + } + } + + *parent = other; + free(parent_node); + return 0; +} + +void trie_remove(struct trie *trie, struct trie_leaf *leaf) { + uintptr_t *child = &trie->root; + uintptr_t *parent = NULL; + unsigned int child_bit = 0, child_index = 0; + size_t offset = 0; + while (!trie_is_leaf(*child)) { + struct trie_node *node = trie_decode_node(*child); + offset += node->offset; + assert((offset >> 1) < leaf->length); + + unsigned char nibble = trie_key_nibble(leaf->key, offset); + unsigned int bit = 1U << nibble; + unsigned int bitmap = node->bitmap; + assert(bitmap & bit); + unsigned int index = trie_popcount(bitmap & (bit - 1)); + + // Advance the parent pointer, unless this node had only one child + if (bitmap & (bitmap - 1)) { + parent = child; + child_bit = bit; + child_index = index; + } + + child = &node->children[index]; + } + + assert(trie_decode_leaf(*child) == leaf); + + if (!parent) { + trie_free_singletons(trie->root); + trie->root = 0; + return; + } + + struct trie_node *node = trie_decode_node(*parent); + child = node->children + child_index; + trie_free_singletons(*child); + + node->bitmap ^= child_bit; + unsigned int parent_size = trie_popcount(node->bitmap); + assert(parent_size > 0); + if (parent_size == 1 && trie_collapse_node(parent, node, child_index) == 0) { + return; + } + + if (child_index < parent_size) { + memmove(child, child + 1, (parent_size - child_index)*sizeof(*child)); + } + + if ((parent_size & (parent_size - 1)) == 0) { + node = realloc(node, trie_node_size(parent_size)); + if (node) { + *parent = trie_encode_node(node); + } + } +} + +/** Free an encoded pointer to a node. */ +static void free_trie_ptr(uintptr_t ptr) { + if (trie_is_leaf(ptr)) { + free(trie_decode_leaf(ptr)); + } else { + struct trie_node *node = trie_decode_node(ptr); + size_t size = trie_popcount(node->bitmap); + for (size_t i = 0; i < size; ++i) { + free_trie_ptr(node->children[i]); + } + free(node); + } +} + +void trie_destroy(struct trie *trie) { + if (trie->root) { + free_trie_ptr(trie->root); + } +} diff --git a/src/trie.h b/src/trie.h new file mode 100644 index 0000000..2d29ac7 --- /dev/null +++ b/src/trie.h @@ -0,0 +1,156 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2019 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. * + ****************************************************************************/ + +#ifndef BFS_TRIE_H +#define BFS_TRIE_H + +#include <stddef.h> +#include <stdint.h> + +/** + * A trie that holds a set of fixed- or variable-length strings. + */ +struct trie { + uintptr_t root; +}; + +/** + * A leaf of a trie. + */ +struct trie_leaf { + /** + * An arbitrary value associated with this leaf. + */ + void *value; + + /** + * The length of the key in bytes. + */ + size_t length; + + /** + * The key itself, stored inline. + */ + char key[]; +}; + +/** + * Initialize an empty trie. + */ +void trie_init(struct trie *trie); + +/** + * Get the first (lexicographically earliest) leaf in the trie. + * + * @param trie + * The trie to search. + * @return + * The first leaf, or NULL if the trie is empty. + */ +struct trie_leaf *trie_first_leaf(const struct trie *trie); + +/** + * Find the leaf for a string key. + * + * @param trie + * The trie to search. + * @param key + * The key to look up. + * @return + * The found leaf, or NULL if the key is not present. + */ +struct trie_leaf *trie_find_str(const struct trie *trie, const char *key); + +/** + * Find the leaf for a fixed-size key. + * + * @param trie + * The trie to search. + * @param key + * The key to look up. + * @param length + * The length of the key in bytes. + * @return + * The found leaf, or NULL if the key is not present. + */ +struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length); + +/** + * Find the shortest leaf that starts with a given key. + * + * @param trie + * The trie to search. + * @param key + * The key to look up. + * @return + * A leaf that starts with the given key, or NULL. + */ +struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key); + +/** + * Find the leaf that is the longest prefix of the given key. + * + * @param trie + * The trie to search. + * @param key + * The key to look up. + * @return + * The longest prefix match for the given key, or NULL. + */ +struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key); + +/** + * Insert a string key into the trie. + * + * @param trie + * The trie to modify. + * @param key + * The key to insert. + * @return + * The inserted leaf, or NULL on failure. + */ +struct trie_leaf *trie_insert_str(struct trie *trie, const char *key); + +/** + * Insert a fixed-size key into the trie. + * + * @param trie + * The trie to modify. + * @param key + * The key to insert. + * @param length + * The length of the key in bytes. + * @return + * The inserted leaf, or NULL on failure. + */ +struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length); + +/** + * Remove a leaf from a trie. + * + * @param trie + * The trie to modify. + * @param leaf + * The leaf to remove. + */ +void trie_remove(struct trie *trie, struct trie_leaf *leaf); + +/** + * Destroy a trie and its contents. + */ +void trie_destroy(struct trie *trie); + +#endif // BFS_TRIE_H diff --git a/src/typo.c b/src/typo.c new file mode 100644 index 0000000..4012730 --- /dev/null +++ b/src/typo.c @@ -0,0 +1,176 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2016 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. * + ****************************************************************************/ + +#include "typo.h" +#include <limits.h> +#include <stdlib.h> +#include <string.h> + +// Assume QWERTY layout for now +static const int key_coords[UCHAR_MAX][3] = { + ['`'] = { 0, 0, 0}, + ['~'] = { 0, 0, 1}, + ['1'] = { 3, 0, 0}, + ['!'] = { 3, 0, 1}, + ['2'] = { 6, 0, 0}, + ['@'] = { 6, 0, 1}, + ['3'] = { 9, 0, 0}, + ['#'] = { 9, 0, 1}, + ['4'] = {12, 0, 0}, + ['$'] = {12, 0, 1}, + ['5'] = {15, 0, 0}, + ['%'] = {15, 0, 1}, + ['6'] = {18, 0, 0}, + ['^'] = {18, 0, 1}, + ['7'] = {21, 0, 0}, + ['&'] = {21, 0, 1}, + ['8'] = {24, 0, 0}, + ['*'] = {24, 0, 1}, + ['9'] = {27, 0, 0}, + ['('] = {27, 0, 1}, + ['0'] = {30, 0, 0}, + [')'] = {30, 0, 1}, + ['-'] = {33, 0, 0}, + ['_'] = {33, 0, 1}, + ['='] = {36, 0, 0}, + ['+'] = {36, 0, 1}, + + ['\t'] = { 1, 3, 0}, + ['q'] = { 4, 3, 0}, + ['Q'] = { 4, 3, 1}, + ['w'] = { 7, 3, 0}, + ['W'] = { 7, 3, 1}, + ['e'] = {10, 3, 0}, + ['E'] = {10, 3, 1}, + ['r'] = {13, 3, 0}, + ['R'] = {13, 3, 1}, + ['t'] = {16, 3, 0}, + ['T'] = {16, 3, 1}, + ['y'] = {19, 3, 0}, + ['Y'] = {19, 3, 1}, + ['u'] = {22, 3, 0}, + ['U'] = {22, 3, 1}, + ['i'] = {25, 3, 0}, + ['I'] = {25, 3, 1}, + ['o'] = {28, 3, 0}, + ['O'] = {28, 3, 1}, + ['p'] = {31, 3, 0}, + ['P'] = {31, 3, 1}, + ['['] = {34, 3, 0}, + ['{'] = {34, 3, 1}, + [']'] = {37, 3, 0}, + ['}'] = {37, 3, 1}, + ['\\'] = {40, 3, 0}, + ['|'] = {40, 3, 1}, + + ['a'] = { 5, 6, 0}, + ['A'] = { 5, 6, 1}, + ['s'] = { 8, 6, 0}, + ['S'] = { 8, 6, 1}, + ['d'] = {11, 6, 0}, + ['D'] = {11, 6, 1}, + ['f'] = {14, 6, 0}, + ['F'] = {14, 6, 1}, + ['g'] = {17, 6, 0}, + ['G'] = {17, 6, 1}, + ['h'] = {20, 6, 0}, + ['H'] = {20, 6, 1}, + ['j'] = {23, 6, 0}, + ['J'] = {23, 6, 1}, + ['k'] = {26, 6, 0}, + ['K'] = {26, 6, 1}, + ['l'] = {29, 6, 0}, + ['L'] = {29, 6, 1}, + [';'] = {32, 6, 0}, + [':'] = {32, 6, 1}, + ['\''] = {35, 6, 0}, + ['"'] = {35, 6, 1}, + ['\n'] = {38, 6, 0}, + + ['z'] = { 6, 9, 0}, + ['Z'] = { 6, 9, 1}, + ['x'] = { 9, 9, 0}, + ['X'] = { 9, 9, 1}, + ['c'] = {12, 9, 0}, + ['C'] = {12, 9, 1}, + ['v'] = {15, 9, 0}, + ['V'] = {15, 9, 1}, + ['b'] = {18, 9, 0}, + ['B'] = {18, 9, 1}, + ['n'] = {21, 9, 0}, + ['N'] = {21, 9, 1}, + ['m'] = {24, 9, 0}, + ['M'] = {24, 9, 1}, + [','] = {27, 9, 0}, + ['<'] = {27, 9, 1}, + ['.'] = {30, 9, 0}, + ['>'] = {30, 9, 1}, + ['/'] = {33, 9, 0}, + ['?'] = {33, 9, 1}, + + [' '] = {18, 12, 0}, +}; + +static int char_distance(char a, char b) { + const int *ac = key_coords[(unsigned char)a], *bc = key_coords[(unsigned char)b]; + int ret = 0; + for (int i = 0; i < 3; ++i) { + ret += abs(ac[i] - bc[i]); + } + return ret; +} + +int typo_distance(const char *actual, const char *expected) { + // This is the Wagner-Fischer algorithm for Levenshtein distance, using + // Manhattan distance on the keyboard for individual characters. + + const int insert_cost = 12; + + size_t rows = strlen(actual) + 1; + size_t cols = strlen(expected) + 1; + + int arr0[cols], arr1[cols]; + int *row0 = arr0, *row1 = arr1; + + for (size_t j = 0; j < cols; ++j) { + row0[j] = insert_cost * j; + } + + for (size_t i = 1; i < rows; ++i) { + row1[0] = row0[0] + insert_cost; + + char a = actual[i - 1]; + for (size_t j = 1; j < cols; ++j) { + char b = expected[j - 1]; + int cost = row0[j - 1] + char_distance(a, b); + int del_cost = row0[j] + insert_cost; + if (del_cost < cost) { + cost = del_cost; + } + int ins_cost = row1[j - 1] + insert_cost; + if (ins_cost < cost) { + cost = ins_cost; + } + row1[j] = cost; + } + + int *tmp = row0; + row0 = row1; + row1 = tmp; + } + + return row0[cols - 1]; +} diff --git a/src/typo.h b/src/typo.h new file mode 100644 index 0000000..0347aae --- /dev/null +++ b/src/typo.h @@ -0,0 +1,31 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2016 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. * + ****************************************************************************/ + +#ifndef BFS_TYPO_H +#define BFS_TYPO_H + +/** + * Find the "typo" distance between two strings. + * + * @param actual + * The actual string typed by the user. + * @param expected + * The expected valid string. + * @return The distance between the two strings. + */ +int typo_distance(const char *actual, const char *expected); + +#endif // BFS_TYPO_H diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..a62e66c --- /dev/null +++ b/src/util.c @@ -0,0 +1,510 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2016-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. * + ****************************************************************************/ + +#include "util.h" +#include "dstring.h" +#include "xregex.h" +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <langinfo.h> +#include <nl_types.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <wchar.h> + +#if BFS_HAS_SYS_PARAM +# include <sys/param.h> +#endif + +#if BFS_HAS_SYS_SYSMACROS +# include <sys/sysmacros.h> +#elif BFS_HAS_SYS_MKDEV +# include <sys/mkdev.h> +#endif + +#if BFS_HAS_UTIL +# include <util.h> +#endif + +char *xreadlinkat(int fd, const char *path, size_t size) { + ssize_t len; + char *name = NULL; + + if (size == 0) { + size = 64; + } else { + ++size; // NUL terminator + } + + while (true) { + char *new_name = realloc(name, size); + if (!new_name) { + goto error; + } + name = new_name; + + len = readlinkat(fd, path, name, size); + if (len < 0) { + goto error; + } else if ((size_t)len >= size) { + size *= 2; + } else { + break; + } + } + + name[len] = '\0'; + return name; + +error: + free(name); + return NULL; +} + +int dup_cloexec(int fd) { +#ifdef F_DUPFD_CLOEXEC + return fcntl(fd, F_DUPFD_CLOEXEC, 0); +#else + int ret = dup(fd); + if (ret < 0) { + return -1; + } + + if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) { + close_quietly(ret); + return -1; + } + + return ret; +#endif +} + +int pipe_cloexec(int pipefd[2]) { +#if __linux__ || (BSD && !__APPLE__) + return pipe2(pipefd, O_CLOEXEC); +#else + if (pipe(pipefd) != 0) { + return -1; + } + + if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) { + close_quietly(pipefd[1]); + close_quietly(pipefd[0]); + return -1; + } + + return 0; +#endif +} + +/** Get the single character describing the given file type. */ +static char type_char(mode_t mode) { + switch (mode & S_IFMT) { + case S_IFREG: + return '-'; + case S_IFBLK: + return 'b'; + case S_IFCHR: + return 'c'; + case S_IFDIR: + return 'd'; + case S_IFLNK: + return 'l'; + case S_IFIFO: + return 'p'; + case S_IFSOCK: + return 's'; +#ifdef S_IFDOOR + case S_IFDOOR: + return 'D'; +#endif +#ifdef S_IFPORT + case S_IFPORT: + return 'P'; +#endif +#ifdef S_IFWHT + case S_IFWHT: + return 'w'; +#endif + } + + return '?'; +} + +void xstrmode(mode_t mode, char str[11]) { + strcpy(str, "----------"); + + str[0] = type_char(mode); + + if (mode & 00400) { + str[1] = 'r'; + } + if (mode & 00200) { + str[2] = 'w'; + } + if ((mode & 04100) == 04000) { + str[3] = 'S'; + } else if (mode & 04000) { + str[3] = 's'; + } else if (mode & 00100) { + str[3] = 'x'; + } + + if (mode & 00040) { + str[4] = 'r'; + } + if (mode & 00020) { + str[5] = 'w'; + } + if ((mode & 02010) == 02000) { + str[6] = 'S'; + } else if (mode & 02000) { + str[6] = 's'; + } else if (mode & 00010) { + str[6] = 'x'; + } + + if (mode & 00004) { + str[7] = 'r'; + } + if (mode & 00002) { + str[8] = 'w'; + } + if ((mode & 01001) == 01000) { + str[9] = 'T'; + } else if (mode & 01000) { + str[9] = 't'; + } else if (mode & 00001) { + str[9] = 'x'; + } +} + +const char *xbasename(const char *path) { + const char *i; + + // Skip trailing slashes + for (i = path + strlen(path); i > path && i[-1] == '/'; --i); + + // Find the beginning of the name + for (; i > path && i[-1] != '/'; --i); + + // Skip leading slashes + for (; i[0] == '/' && i[1]; ++i); + + return i; +} + +int xfaccessat(int fd, const char *path, int amode) { + int ret = faccessat(fd, path, amode, 0); + +#ifdef AT_EACCESS + // Some platforms, like Hurd, only support AT_EACCESS. Other platforms, + // like Android, don't support AT_EACCESS at all. + if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) { + ret = faccessat(fd, path, amode, AT_EACCESS); + } +#endif + + return ret; +} + +int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) { +#if BSD && !__GNU__ + char *str_arg = (char *)*str; + unsigned long set_arg = 0; + unsigned long clear_arg = 0; + +#if __NetBSD__ + int ret = string_to_flags(&str_arg, &set_arg, &clear_arg); +#else + int ret = strtofflags(&str_arg, &set_arg, &clear_arg); +#endif + + *str = str_arg; + *set = set_arg; + *clear = clear_arg; + + if (ret != 0) { + errno = EINVAL; + } + return ret; +#else // !BSD + errno = ENOTSUP; + return -1; +#endif +} + +size_t xstrwidth(const char *str) { + size_t len = strlen(str); + size_t ret = 0; + + mbstate_t mb; + memset(&mb, 0, sizeof(mb)); + + while (len > 0) { + wchar_t wc; + size_t mblen = mbrtowc(&wc, str, len, &mb); + int cwidth; + if (mblen == (size_t)-1) { + // Invalid byte sequence, assume a single-width '?' + mblen = 1; + cwidth = 1; + memset(&mb, 0, sizeof(mb)); + } else if (mblen == (size_t)-2) { + // Incomplete byte sequence, assume a single-width '?' + mblen = len; + cwidth = 1; + } else { + cwidth = wcwidth(wc); + if (cwidth < 0) { + cwidth = 0; + } + } + + str += mblen; + len -= mblen; + ret += cwidth; + } + + return ret; +} + +bool is_nonexistence_error(int error) { + return error == ENOENT || errno == ENOTDIR; +} + +/** Compile and execute a regular expression for xrpmatch(). */ +static int xrpregex(nl_item item, const char *response) { + const char *pattern = nl_langinfo(item); + if (!pattern) { + return -1; + } + + struct bfs_regex *regex; + int ret = bfs_regcomp(®ex, pattern, BFS_REGEX_POSIX_EXTENDED, 0); + if (ret == 0) { + ret = bfs_regexec(regex, response, 0); + } + + bfs_regfree(regex); + return ret; +} + +/** Check if a response is affirmative or negative. */ +static int xrpmatch(const char *response) { + int ret = xrpregex(NOEXPR, response); + if (ret > 0) { + return 0; + } else if (ret < 0) { + return -1; + } + + ret = xrpregex(YESEXPR, response); + if (ret > 0) { + return 1; + } else if (ret < 0) { + return -1; + } + + // Failsafe: always handle y/n + char c = response[0]; + if (c == 'n' || c == 'N') { + return 0; + } else if (c == 'y' || c == 'Y') { + return 1; + } else { + return -1; + } +} + +int ynprompt(void) { + fflush(stderr); + + char *line = xgetdelim(stdin, '\n'); + int ret = line ? xrpmatch(line) : -1; + free(line); + return ret; +} + +dev_t bfs_makedev(int ma, int mi) { +#ifdef makedev + return makedev(ma, mi); +#else + return (ma << 8) | mi; +#endif +} + +int bfs_major(dev_t dev) { +#ifdef major + return major(dev); +#else + return dev >> 8; +#endif +} + +int bfs_minor(dev_t dev) { +#ifdef minor + return minor(dev); +#else + return dev & 0xFF; +#endif +} + +size_t xread(int fd, void *buf, size_t nbytes) { + size_t count = 0; + + while (count < nbytes) { + ssize_t ret = read(fd, (char *)buf + count, nbytes - count); + if (ret < 0) { + if (errno == EINTR) { + continue; + } else { + break; + } + } else if (ret == 0) { + // EOF + errno = 0; + break; + } else { + count += ret; + } + } + + return count; +} + +size_t xwrite(int fd, const void *buf, size_t nbytes) { + size_t count = 0; + + while (count < nbytes) { + ssize_t ret = write(fd, (const char *)buf + count, nbytes - count); + if (ret < 0) { + if (errno == EINTR) { + continue; + } else { + break; + } + } else if (ret == 0) { + // EOF? + errno = 0; + break; + } else { + count += ret; + } + } + + return count; +} + +char *xconfstr(int name) { + size_t len = confstr(name, NULL, 0); + if (len == 0) { + return NULL; + } + + char *str = malloc(len); + if (!str) { + return NULL; + } + + if (confstr(name, str, len) != len) { + free(str); + return NULL; + } + + return str; +} + +char *xgetdelim(FILE *file, char delim) { + char *chunk = NULL; + size_t n = 0; + ssize_t len = getdelim(&chunk, &n, delim, file); + if (len >= 0) { + if (chunk[len] == delim) { + chunk[len] = '\0'; + } + return chunk; + } else { + free(chunk); + if (!ferror(file)) { + errno = 0; + } + return NULL; + } +} + +FILE *xfopen(const char *path, int flags) { + char mode[4]; + + switch (flags & O_ACCMODE) { + case O_RDONLY: + strcpy(mode, "rb"); + break; + case O_WRONLY: + strcpy(mode, "wb"); + break; + case O_RDWR: + strcpy(mode, "r+b"); + break; + default: + assert(!"Invalid access mode"); + errno = EINVAL; + return NULL; + } + + if (flags & O_APPEND) { + mode[0] = 'a'; + } + + int fd; + if (flags & O_CREAT) { + fd = open(path, flags, 0666); + } else { + fd = open(path, flags); + } + + if (fd < 0) { + return NULL; + } + + FILE *ret = fdopen(fd, mode); + if (!ret) { + close_quietly(fd); + return NULL; + } + + return ret; +} + +int xclose(int fd) { + int ret = close(fd); + if (ret != 0) { + assert(errno != EBADF); + } + return ret; +} + +void close_quietly(int fd) { + int error = errno; + xclose(fd); + errno = error; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..b5c7d80 --- /dev/null +++ b/src/util.h @@ -0,0 +1,317 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2016-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. * + ****************************************************************************/ + +/** + * Assorted utilities that don't belong anywhere else. + */ + +#ifndef BFS_UTIL_H +#define BFS_UTIL_H + +#include <fcntl.h> +#include <fnmatch.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <sys/types.h> + +// Some portability concerns + +#ifdef __has_feature +# define BFS_HAS_FEATURE(feature, fallback) __has_feature(feature) +#else +# define BFS_HAS_FEATURE(feature, fallback) fallback +#endif + +#ifdef __has_include +# define BFS_HAS_INCLUDE(header, fallback) __has_include(header) +#else +# define BFS_HAS_INCLUDE(header, fallback) fallback +#endif + +#ifdef __has_c_attribute +# define BFS_HAS_C_ATTRIBUTE(attr) __has_c_attribute(attr) +#else +# define BFS_HAS_C_ATTRIBUTE(attr) false +#endif + +#if __GNUC__ && defined(__has_attribute) +# define BFS_HAS_GNU_ATTRIBUTE(attr) __has_attribute(attr) +#else +# define BFS_HAS_GNU_ATTRIBUTE(attr) false +#endif + +#ifndef BFS_HAS_MNTENT +# define BFS_HAS_MNTENT BFS_HAS_INCLUDE(<mntent.h>, __GLIBC__) +#endif + +#ifndef BFS_HAS_SYS_ACL +# define BFS_HAS_SYS_ACL BFS_HAS_INCLUDE(<sys/acl.h>, true) +#endif + +#ifndef BFS_HAS_SYS_CAPABILITY +# define BFS_HAS_SYS_CAPABILITY BFS_HAS_INCLUDE(<sys/capability.h>, __linux__) +#endif + +#ifndef BFS_HAS_SYS_EXTATTR +# define BFS_HAS_SYS_EXTATTR BFS_HAS_INCLUDE(<sys/extattr.h>, __FreeBSD__) +#endif + +#ifndef BFS_HAS_SYS_MKDEV +# define BFS_HAS_SYS_MKDEV BFS_HAS_INCLUDE(<sys/mkdev.h>, false) +#endif + +#ifndef BFS_HAS_SYS_PARAM +# define BFS_HAS_SYS_PARAM BFS_HAS_INCLUDE(<sys/param.h>, true) +#endif + +#ifndef BFS_HAS_SYS_SYSMACROS +# define BFS_HAS_SYS_SYSMACROS BFS_HAS_INCLUDE(<sys/sysmacros.h>, __GLIBC__) +#endif + +#ifndef BFS_HAS_SYS_XATTR +# define BFS_HAS_SYS_XATTR BFS_HAS_INCLUDE(<sys/xattr.h>, __linux__) +#endif + +#ifndef BFS_HAS_UTIL +# define BFS_HAS_UTIL BFS_HAS_INCLUDE(<util.h>, __NetBSD__) +#endif + +#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE) +# define FNM_CASEFOLD FNM_IGNORECASE +#endif + +#ifndef O_DIRECTORY +# define O_DIRECTORY 0 +#endif + +#if BFS_HAS_C_ATTRIBUTE(fallthrough) +# define BFS_FALLTHROUGH [[fallthrough]] +#elif BFS_HAS_GNU_ATTRIBUTE(fallthrough) +# define BFS_FALLTHROUGH __attribute__((fallthrough)) +#else +# define BFS_FALLTHROUGH ((void)0) +#endif + +/** + * Adds compiler warnings for bad printf()-style function calls, if supported. + */ +#if BFS_HAS_GNU_ATTRIBUTE(format) +# define BFS_FORMATTER(fmt, args) __attribute__((format(printf, fmt, args))) +#else +# define BFS_FORMATTER(fmt, args) +#endif + +// Lower bound on BFS_FLEX_SIZEOF() +#define BFS_FLEX_LB(type, member, length) (offsetof(type, member) + sizeof(((type *)NULL)->member[0]) * (length)) + +// Maximum macro for BFS_FLEX_SIZE() +#define BFS_FLEX_MAX(a, b) ((a) > (b) ? (a) : (b)) + +/** + * Computes the size of a struct containing a flexible array member of the given + * length. + * + * @param type + * The type of the struct containing the flexible array. + * @param member + * The name of the flexible array member. + * @param length + * The length of the flexible array. + */ +#define BFS_FLEX_SIZEOF(type, member, length) \ + (sizeof(type) <= BFS_FLEX_LB(type, member, 0) \ + ? BFS_FLEX_LB(type, member, length) \ + : BFS_FLEX_MAX(sizeof(type), BFS_FLEX_LB(type, member, length))) + +/** + * readlinkat() wrapper that dynamically allocates the result. + * + * @param fd + * The base directory descriptor. + * @param path + * The path to the link, relative to fd. + * @param size + * An estimate for the size of the link name (pass 0 if unknown). + * @return The target of the link, allocated with malloc(), or NULL on failure. + */ +char *xreadlinkat(int fd, const char *path, size_t size); + +/** + * Like dup(), but set the FD_CLOEXEC flag. + * + * @param fd + * The file descriptor to duplicate. + * @return A duplicated file descriptor, or -1 on failure. + */ +int dup_cloexec(int fd); + +/** + * Like pipe(), but set the FD_CLOEXEC flag. + * + * @param pipefd + * The array to hold the two file descriptors. + * @return 0 on success, -1 on failure. + */ +int pipe_cloexec(int pipefd[2]); + +/** + * Format a mode like ls -l (e.g. -rw-r--r--). + * + * @param mode + * The mode to format. + * @param str + * The string to hold the formatted mode. + */ +void xstrmode(mode_t mode, char str[11]); + +/** + * basename() variant that doesn't modify the input. + * + * @param path + * The path in question. + * @return A pointer into path at the base name offset. + */ +const char *xbasename(const char *path); + +/** + * Wrapper for faccessat() that handles some portability issues. + */ +int xfaccessat(int fd, const char *path, int amode); + +/** + * Portability wrapper for strtofflags(). + * + * @param str + * The string to parse. The pointee will be advanced to the first + * invalid position on error. + * @param set + * The flags that are set in the string. + * @param clear + * The flags that are cleared in the string. + * @return + * 0 on success, -1 on failure. + */ +int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear); + +/** + * wcswidth() variant that works on narrow strings. + * + * @param str + * The string to measure. + * @return + * The likely width of that string in a terminal. + */ +size_t xstrwidth(const char *str); + +/** + * Return whether an error code is due to a path not existing. + */ +bool is_nonexistence_error(int error); + +/** + * Process a yes/no prompt. + * + * @return 1 for yes, 0 for no, and -1 for unknown. + */ +int ynprompt(void); + +/** + * Portable version of makedev(). + */ +dev_t bfs_makedev(int ma, int mi); + +/** + * Portable version of major(). + */ +int bfs_major(dev_t dev); + +/** + * Portable version of minor(). + */ +int bfs_minor(dev_t dev); + +/** + * A safe version of read() that handles interrupted system calls and partial + * reads. + * + * @return + * The number of bytes read. A value != nbytes indicates an error + * (errno != 0) or end of file (errno == 0). + */ +size_t xread(int fd, void *buf, size_t nbytes); + +/** + * A safe version of write() that handles interrupted system calls and partial + * writes. + * + * @return + The number of bytes written. A value != nbytes indicates an error. + */ +size_t xwrite(int fd, const void *buf, size_t nbytes); + +/** + * Wrapper for confstr() that allocates with malloc(). + * + * @param name + * The ID of the confstr to look up. + * @return + * The value of the confstr, or NULL on failure. + */ +char *xconfstr(int name); + +/** + * Convenience wrapper for getdelim(). + * + * @param file + * The file to read. + * @param delim + * The delimiter character to split on. + * @return + * The read chunk (without the delimiter), allocated with malloc(). + * NULL is returned on error (errno != 0) or end of file (errno == 0). + */ +char *xgetdelim(FILE *file, char delim); + +/** + * fopen() variant that takes open() style flags. + * + * @param path + * The path to open. + * @param flags + * Flags to pass to open(). + */ +FILE *xfopen(const char *path, int flags); + +/** + * close() wrapper that asserts the file descriptor is valid. + * + * @param fd + * The file descriptor to close. + * @return + * 0 on success, or -1 on error. + */ +int xclose(int fd); + +/** + * close() variant that preserves errno. + * + * @param fd + * The file descriptor to close. + */ +void close_quietly(int fd); + +#endif // BFS_UTIL_H diff --git a/src/xregex.c b/src/xregex.c new file mode 100644 index 0000000..3c3cf35 --- /dev/null +++ b/src/xregex.c @@ -0,0 +1,301 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 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. * + ****************************************************************************/ + +#include "xregex.h" +#include "util.h" +#include <assert.h> +#include <errno.h> +#include <stdlib.h> +#include <string.h> + +#if BFS_WITH_ONIGURUMA +# include <langinfo.h> +# include <oniguruma.h> +#else +# include <regex.h> +#endif + +struct bfs_regex { +#if BFS_WITH_ONIGURUMA + unsigned char *pattern; + OnigRegex impl; + int err; + OnigErrorInfo einfo; +#else + regex_t impl; + int err; +#endif +}; + +#if BFS_WITH_ONIGURUMA +/** Get (and initialize) the appropriate encoding for the current locale. */ +static int bfs_onig_encoding(OnigEncoding *penc) { + static OnigEncoding enc = NULL; + if (enc) { + *penc = enc; + return ONIG_NORMAL; + } + + // Fall back to ASCII by default + enc = ONIG_ENCODING_ASCII; + + // Oniguruma has no locale support, so try to guess the right encoding + // from the current locale. + const char *charmap = nl_langinfo(CODESET); + if (charmap) { +#define BFS_MAP_ENCODING(name, value) \ + do { \ + if (strcmp(charmap, name) == 0) { \ + enc = value; \ + } \ + } while (0) +#define BFS_MAP_ENCODING2(name1, name2, value) \ + do { \ + BFS_MAP_ENCODING(name1, value); \ + BFS_MAP_ENCODING(name2, value); \ + } while (0) + + // These names were found with locale -m on Linux and FreeBSD +#define BFS_MAP_ISO_8859(n) \ + BFS_MAP_ENCODING2("ISO-8859-" #n, "ISO8859-" #n, ONIG_ENCODING_ISO_8859_ ## n) + + BFS_MAP_ISO_8859(1); + BFS_MAP_ISO_8859(2); + BFS_MAP_ISO_8859(3); + BFS_MAP_ISO_8859(4); + BFS_MAP_ISO_8859(5); + BFS_MAP_ISO_8859(6); + BFS_MAP_ISO_8859(7); + BFS_MAP_ISO_8859(8); + BFS_MAP_ISO_8859(9); + BFS_MAP_ISO_8859(10); + BFS_MAP_ISO_8859(11); + // BFS_MAP_ISO_8859(12); + BFS_MAP_ISO_8859(13); + BFS_MAP_ISO_8859(14); + BFS_MAP_ISO_8859(15); + BFS_MAP_ISO_8859(16); + + BFS_MAP_ENCODING("UTF-8", ONIG_ENCODING_UTF8); + +#define BFS_MAP_EUC(name) \ + BFS_MAP_ENCODING2("EUC-" #name, "euc" #name, ONIG_ENCODING_EUC_ ## name) + + BFS_MAP_EUC(JP); + BFS_MAP_EUC(TW); + BFS_MAP_EUC(KR); + BFS_MAP_EUC(CN); + + BFS_MAP_ENCODING2("SHIFT_JIS", "SJIS", ONIG_ENCODING_SJIS); + + // BFS_MAP_ENCODING("KOI-8", ONIG_ENCODING_KOI8); + BFS_MAP_ENCODING("KOI8-R", ONIG_ENCODING_KOI8_R); + + BFS_MAP_ENCODING("CP1251", ONIG_ENCODING_CP1251); + + BFS_MAP_ENCODING("GB18030", ONIG_ENCODING_BIG5); + } + + int ret = onig_initialize(&enc, 1); + if (ret != ONIG_NORMAL) { + enc = NULL; + } + *penc = enc; + return ret; +} +#endif + +int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_type type, enum bfs_regcomp_flags flags) { + struct bfs_regex *regex = *preg = malloc(sizeof(*regex)); + if (!regex) { + return -1; + } + +#if BFS_WITH_ONIGURUMA + // onig_error_code_to_str() says + // + // don't call this after the pattern argument of onig_new() is freed + // + // so make a defensive copy. + regex->pattern = (unsigned char *)strdup(pattern); + if (!regex->pattern) { + goto fail; + } + + regex->impl = NULL; + regex->err = ONIG_NORMAL; + + OnigSyntaxType *syntax = NULL; + switch (type) { + case BFS_REGEX_POSIX_BASIC: + syntax = ONIG_SYNTAX_POSIX_BASIC; + break; + case BFS_REGEX_POSIX_EXTENDED: + syntax = ONIG_SYNTAX_POSIX_EXTENDED; + break; + case BFS_REGEX_EMACS: + syntax = ONIG_SYNTAX_EMACS; + break; + case BFS_REGEX_GREP: + syntax = ONIG_SYNTAX_GREP; + break; + } + assert(syntax); + + OnigOptionType options = syntax->options; + if (flags & BFS_REGEX_ICASE) { + options |= ONIG_OPTION_IGNORECASE; + } + + OnigEncoding enc; + regex->err = bfs_onig_encoding(&enc); + if (regex->err != ONIG_NORMAL) { + return -1; + } + + const unsigned char *end = regex->pattern + strlen(pattern); + regex->err = onig_new(®ex->impl, regex->pattern, end, options, enc, syntax, ®ex->einfo); + if (regex->err != ONIG_NORMAL) { + return -1; + } +#else + int cflags = 0; + switch (type) { + case BFS_REGEX_POSIX_BASIC: + break; + case BFS_REGEX_POSIX_EXTENDED: + cflags |= REG_EXTENDED; + break; + default: + errno = EINVAL; + goto fail; + } + + if (flags & BFS_REGEX_ICASE) { + cflags |= REG_ICASE; + } + +#if BFS_HAS_FEATURE(memory_sanitizer, false) + // https://github.com/google/sanitizers/issues/1496 + memset(®ex->impl, 0, sizeof(regex->impl)); +#endif + + regex->err = regcomp(®ex->impl, pattern, cflags); + if (regex->err != 0) { + return -1; + } +#endif + + return 0; + +fail: + free(regex); + *preg = NULL; + return -1; +} + +int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags) { + size_t len = strlen(str); + +#if BFS_WITH_ONIGURUMA + const unsigned char *ustr = (const unsigned char *)str; + const unsigned char *end = ustr + len; + + // The docs for onig_{match,search}() say + // + // Do not pass invalid byte string in the regex character encoding. + if (!onigenc_is_valid_mbc_string(onig_get_encoding(regex->impl), ustr, end)) { + return 0; + } + + int ret; + if (flags & BFS_REGEX_ANCHOR) { + ret = onig_match(regex->impl, ustr, end, ustr, NULL, ONIG_OPTION_DEFAULT); + } else { + ret = onig_search(regex->impl, ustr, end, ustr, end, NULL, ONIG_OPTION_DEFAULT); + } + + if (ret >= 0) { + if (flags & BFS_REGEX_ANCHOR) { + return (size_t)ret == len; + } else { + return 1; + } + } else if (ret == ONIG_MISMATCH) { + return 0; + } else { + regex->err = ret; + return -1; + } +#else + regmatch_t match = { + .rm_so = 0, + .rm_eo = len, + }; + + int eflags = 0; +#ifdef REG_STARTEND + eflags |= REG_STARTEND; +#endif + + int ret = regexec(®ex->impl, str, 1, &match, eflags); + if (ret == 0) { + if (flags & BFS_REGEX_ANCHOR) { + return match.rm_so == 0 && (size_t)match.rm_eo == len; + } else { + return 1; + } + } else if (ret == REG_NOMATCH) { + return 0; + } else { + regex->err = ret; + return -1; + } +#endif +} + +void bfs_regfree(struct bfs_regex *regex) { + if (regex) { +#if BFS_WITH_ONIGURUMA + onig_free(regex->impl); + free(regex->pattern); +#else + regfree(®ex->impl); +#endif + free(regex); + } +} + +char *bfs_regerror(const struct bfs_regex *regex) { + if (!regex) { + return strdup(strerror(ENOMEM)); + } + +#if BFS_WITH_ONIGURUMA + unsigned char *str = malloc(ONIG_MAX_ERROR_MESSAGE_LEN); + if (str) { + onig_error_code_to_str(str, regex->err, ®ex->einfo); + } + return (char *)str; +#else + size_t len = regerror(regex->err, ®ex->impl, NULL, 0); + char *str = malloc(len); + if (str) { + regerror(regex->err, ®ex->impl, str, len); + } + return str; +#endif +} diff --git a/src/xregex.h b/src/xregex.h new file mode 100644 index 0000000..b2f56a5 --- /dev/null +++ b/src/xregex.h @@ -0,0 +1,97 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2022 Tavian Barnes <tavianator@tavianator.com> and bfs * + * contributors * + * * + * 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. * + ****************************************************************************/ + +#ifndef BFS_XREGEX_H +#define BFS_XREGEX_H + +/** + * A compiled regular expression. + */ +struct bfs_regex; + +/** + * Regex syntax flavors. + */ +enum bfs_regex_type { + BFS_REGEX_POSIX_BASIC, + BFS_REGEX_POSIX_EXTENDED, + BFS_REGEX_EMACS, + BFS_REGEX_GREP, +}; + +/** + * Regex compilation flags. + */ +enum bfs_regcomp_flags { + /** Treat the regex case-insensitively. */ + BFS_REGEX_ICASE = 1 << 0, +}; + +/** + * Regex execution flags. + */ +enum bfs_regexec_flags { + /** Only treat matches of the entire string as successful. */ + BFS_REGEX_ANCHOR = 1 << 0, +}; + +/** + * Wrapper for regcomp() that supports additional regex types. + * + * @param[out] preg + * Will hold the compiled regex. + * @param pattern + * The regular expression to compile. + * @param type + * The regular expression syntax to use. + * @param flags + * Regex compilation flags. + * @return + * 0 on success, -1 on failure. + */ +int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_type type, enum bfs_regcomp_flags flags); + +/** + * Wrapper for regexec(). + * + * @param regex + * The regular expression to execute. + * @param str + * The string to match against. + * @param flags + * Regex execution flags. + * @return + * 1 for a match, 0 for no match, -1 on failure. + */ +int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags); + +/** + * Free a compiled regex. + */ +void bfs_regfree(struct bfs_regex *regex); + +/** + * Get a human-readable regex error message. + * + * @param regex + * The compiled regex. + * @return + * A human-readable description of the error, which should be free()'d. + */ +char *bfs_regerror(const struct bfs_regex *regex); + +#endif // BFS_XREGEX_H diff --git a/src/xspawn.c b/src/xspawn.c new file mode 100644 index 0000000..93c270a --- /dev/null +++ b/src/xspawn.c @@ -0,0 +1,318 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2018-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. * + ****************************************************************************/ + +#include "xspawn.h" +#include "util.h" +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +/** + * Types of spawn actions. + */ +enum bfs_spawn_op { + BFS_SPAWN_CLOSE, + BFS_SPAWN_DUP2, + BFS_SPAWN_FCHDIR, + BFS_SPAWN_SETRLIMIT, +}; + +/** + * A spawn action. + */ +struct bfs_spawn_action { + struct bfs_spawn_action *next; + + enum bfs_spawn_op op; + int in_fd; + int out_fd; + int resource; + struct rlimit rlimit; +}; + +int bfs_spawn_init(struct bfs_spawn *ctx) { + ctx->flags = 0; + ctx->actions = NULL; + ctx->tail = &ctx->actions; + return 0; +} + +int bfs_spawn_destroy(struct bfs_spawn *ctx) { + struct bfs_spawn_action *action = ctx->actions; + while (action) { + struct bfs_spawn_action *next = action->next; + free(action); + action = next; + } + return 0; +} + +int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags) { + ctx->flags = flags; + return 0; +} + +/** Add a spawn action to the chain. */ +static struct bfs_spawn_action *bfs_spawn_add(struct bfs_spawn *ctx, enum bfs_spawn_op op) { + struct bfs_spawn_action *action = malloc(sizeof(*action)); + if (action) { + action->next = NULL; + action->op = op; + action->in_fd = -1; + action->out_fd = -1; + + *ctx->tail = action; + ctx->tail = &action->next; + } + return action; +} + +int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { + if (fd < 0) { + errno = EBADF; + return -1; + } + + struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_CLOSE); + if (action) { + action->out_fd = fd; + return 0; + } else { + return -1; + } +} + +int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) { + if (oldfd < 0 || newfd < 0) { + errno = EBADF; + return -1; + } + + struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_DUP2); + if (action) { + action->in_fd = oldfd; + action->out_fd = newfd; + return 0; + } else { + return -1; + } +} + +int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) { + if (fd < 0) { + errno = EBADF; + return -1; + } + + struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_FCHDIR); + if (action) { + action->in_fd = fd; + return 0; + } else { + return -1; + } +} + +int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) { + struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_SETRLIMIT); + if (action) { + action->resource = resource; + action->rlimit = *rl; + return 0; + } else { + return -1; + } +} + +/** Actually exec() the new process. */ +static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { + int error; + const struct bfs_spawn_action *actions = ctx ? ctx->actions : NULL; + + xclose(pipefd[0]); + + for (const struct bfs_spawn_action *action = actions; action; action = action->next) { + // Move the error-reporting pipe out of the way if necessary... + if (action->out_fd == pipefd[1]) { + int fd = dup_cloexec(pipefd[1]); + if (fd < 0) { + goto fail; + } + xclose(pipefd[1]); + pipefd[1] = fd; + } + + // ... and pretend the pipe doesn't exist + if (action->in_fd == pipefd[1]) { + errno = EBADF; + goto fail; + } + + switch (action->op) { + case BFS_SPAWN_CLOSE: + if (close(action->out_fd) != 0) { + goto fail; + } + break; + case BFS_SPAWN_DUP2: + if (dup2(action->in_fd, action->out_fd) < 0) { + goto fail; + } + break; + case BFS_SPAWN_FCHDIR: + if (fchdir(action->in_fd) != 0) { + goto fail; + } + break; + case BFS_SPAWN_SETRLIMIT: + if (setrlimit(action->resource, &action->rlimit) != 0) { + goto fail; + } + break; + } + } + + execve(exe, argv, envp); + +fail: + error = errno; + + // In case of a write error, the parent will still see that we exited + // unsuccessfully, but won't know why + (void) xwrite(pipefd[1], &error, sizeof(error)); + + xclose(pipefd[1]); + _Exit(127); +} + +pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { + extern char **environ; + if (!envp) { + envp = environ; + } + + enum bfs_spawn_flags flags = ctx ? ctx->flags : 0; + char *resolved = NULL; + if (flags & BFS_SPAWN_USEPATH) { + exe = resolved = bfs_spawn_resolve(exe); + if (!resolved) { + return -1; + } + } + + // Use a pipe to report errors from the child + int pipefd[2]; + if (pipe_cloexec(pipefd) != 0) { + free(resolved); + return -1; + } + + pid_t pid = fork(); + if (pid < 0) { + close_quietly(pipefd[1]); + close_quietly(pipefd[0]); + free(resolved); + return -1; + } else if (pid == 0) { + // Child + bfs_spawn_exec(exe, ctx, argv, envp, pipefd); + } + + // Parent + xclose(pipefd[1]); + free(resolved); + + int error; + ssize_t nbytes = xread(pipefd[0], &error, sizeof(error)); + xclose(pipefd[0]); + if (nbytes == sizeof(error)) { + int wstatus; + waitpid(pid, &wstatus, 0); + errno = error; + return -1; + } + + return pid; +} + +char *bfs_spawn_resolve(const char *exe) { + if (strchr(exe, '/')) { + return strdup(exe); + } + + const char *path = getenv("PATH"); + + char *confpath = NULL; + if (!path) { + path = confpath = xconfstr(_CS_PATH); + if (!path) { + return NULL; + } + } + + size_t cap = 0; + char *ret = NULL; + while (true) { + const char *end = strchr(path, ':'); + size_t len = end ? (size_t)(end - path) : strlen(path); + + // POSIX 8.3: "A zero-length prefix is a legacy feature that + // indicates the current working directory." + if (len == 0) { + path = "."; + len = 1; + } + + size_t total = len + 1 + strlen(exe) + 1; + if (cap < total) { + char *grown = realloc(ret, total); + if (!grown) { + goto fail; + } + ret = grown; + cap = total; + } + + memcpy(ret, path, len); + if (ret[len - 1] != '/') { + ret[len++] = '/'; + } + strcpy(ret + len, exe); + + if (xfaccessat(AT_FDCWD, ret, X_OK) == 0) { + break; + } + + if (!end) { + errno = ENOENT; + goto fail; + } + + path = end + 1; + } + + free(confpath); + return ret; + +fail: + free(confpath); + free(ret); + return NULL; +} diff --git a/src/xspawn.h b/src/xspawn.h new file mode 100644 index 0000000..cd6a42e --- /dev/null +++ b/src/xspawn.h @@ -0,0 +1,123 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2018-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. * + ****************************************************************************/ + +/** + * A process-spawning library inspired by posix_spawn(). + */ + +#ifndef BFS_XSPAWN_H +#define BFS_XSPAWN_H + +#include <sys/resource.h> +#include <sys/types.h> + +/** + * bfs_spawn() flags. + */ +enum bfs_spawn_flags { + /** Use the PATH variable to resolve the executable (like execvp()). */ + BFS_SPAWN_USEPATH = 1 << 0, +}; + +/** + * bfs_spawn() attributes, controlling the context of the new process. + */ +struct bfs_spawn { + enum bfs_spawn_flags flags; + struct bfs_spawn_action *actions; + struct bfs_spawn_action **tail; +}; + +/** + * Create a new bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_init(struct bfs_spawn *ctx); + +/** + * Destroy a bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_destroy(struct bfs_spawn *ctx); + +/** + * Set the flags for a bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags); + +/** + * Add a close() action to a bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd); + +/** + * Add a dup2() action to a bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd); + +/** + * Add an fchdir() action to a bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd); + +/** + * Add a setrlimit() action to a bfs_spawn() context. + * + * @return 0 on success, -1 on failure. + */ +int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl); + +/** + * Spawn a new process. + * + * @param exe + * The executable to run. + * @param ctx + * The context for the new process. + * @param argv + * The arguments for the new process. + * @param envp + * The environment variables for the new process (NULL for the current + * environment). + * @return + * The PID of the new process, or -1 on error. + */ +pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp); + +/** + * Look up an executable in the current PATH, as BFS_SPAWN_USEPATH or execvp() + * would do. + * + * @param exe + * The name of the binary to execute. Bare names without a '/' will be + * searched on the provided PATH. + * @return + * The full path to the executable, which should be free()'d, or NULL on + * failure. + */ +char *bfs_spawn_resolve(const char *exe); + +#endif // BFS_XSPAWN_H diff --git a/src/xtime.c b/src/xtime.c new file mode 100644 index 0000000..8ca963b --- /dev/null +++ b/src/xtime.c @@ -0,0 +1,323 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020-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. * + ****************************************************************************/ + +#include "xtime.h" +#include <errno.h> +#include <limits.h> +#include <stdbool.h> +#include <stdlib.h> +#include <time.h> + +/** Whether tzset() has been called. */ +static bool tz_is_set = false; + +int xlocaltime(const time_t *timep, struct tm *result) { + // Should be called before localtime_r() according to POSIX.1-2004 + if (!tz_is_set) { + tzset(); + tz_is_set = true; + } + + if (localtime_r(timep, result)) { + return 0; + } else { + return -1; + } +} + +int xgmtime(const time_t *timep, struct tm *result) { + // Should be called before gmtime_r() according to POSIX.1-2004 + if (!tz_is_set) { + tzset(); + tz_is_set = true; + } + + if (gmtime_r(timep, result)) { + return 0; + } else { + return -1; + } +} + +int xmktime(struct tm *tm, time_t *timep) { + *timep = mktime(tm); + + if (*timep == -1) { + int error = errno; + + struct tm tmp; + if (xlocaltime(timep, &tmp) != 0) { + return -1; + } + + if (tm->tm_year != tmp.tm_year || tm->tm_yday != tmp.tm_yday + || tm->tm_hour != tmp.tm_hour || tm->tm_min != tmp.tm_min || tm->tm_sec != tmp.tm_sec) { + errno = error; + return -1; + } + } + + return 0; +} + +static int safe_add(int *value, int delta) { + if (*value >= 0) { + if (delta > INT_MAX - *value) { + return -1; + } + } else { + if (delta < INT_MIN - *value) { + return -1; + } + } + + *value += delta; + return 0; +} + +static int floor_div(int n, int d) { + int a = n < 0; + return (n + a)/d - a; +} + +static int wrap(int *value, int max, int *next) { + int carry = floor_div(*value, max); + *value -= carry * max; + return safe_add(next, carry); +} + +static int month_length(int year, int month) { + static const int month_lengths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + int ret = month_lengths[month]; + if (month == 1 && year%4 == 0 && (year%100 != 0 || (year + 300)%400 == 0)) { + ++ret; + } + return ret; +} + +int xtimegm(struct tm *tm, time_t *timep) { + tm->tm_isdst = 0; + + if (wrap(&tm->tm_sec, 60, &tm->tm_min) != 0) { + goto overflow; + } + if (wrap(&tm->tm_min, 60, &tm->tm_hour) != 0) { + goto overflow; + } + if (wrap(&tm->tm_hour, 24, &tm->tm_mday) != 0) { + goto overflow; + } + + // In order to wrap the days of the month, we first need to know what + // month it is + if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) { + goto overflow; + } + + if (tm->tm_mday < 1) { + do { + --tm->tm_mon; + if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) { + goto overflow; + } + + tm->tm_mday += month_length(tm->tm_year, tm->tm_mon); + } while (tm->tm_mday < 1); + } else { + while (true) { + int days = month_length(tm->tm_year, tm->tm_mon); + if (tm->tm_mday <= days) { + break; + } + + tm->tm_mday -= days; + ++tm->tm_mon; + if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) { + goto overflow; + } + } + } + + tm->tm_yday = 0; + for (int i = 0; i < tm->tm_mon; ++i) { + tm->tm_yday += month_length(tm->tm_year, i); + } + tm->tm_yday += tm->tm_mday - 1; + + int leap_days; + // Compute floor((year - 69)/4) - floor((year - 1)/100) + floor((year + 299)/400) without overflows + if (tm->tm_year >= 0) { + leap_days = floor_div(tm->tm_year - 69, 4) - floor_div(tm->tm_year - 1, 100) + floor_div(tm->tm_year - 101, 400) + 1; + } else { + leap_days = floor_div(tm->tm_year + 3, 4) - floor_div(tm->tm_year + 99, 100) + floor_div(tm->tm_year + 299, 400) - 17; + } + + long long epoch_days = 365LL*(tm->tm_year - 70) + leap_days + tm->tm_yday; + tm->tm_wday = (epoch_days + 4)%7; + if (tm->tm_wday < 0) { + tm->tm_wday += 7; + } + + long long epoch_time = tm->tm_sec + 60*(tm->tm_min + 60*(tm->tm_hour + 24*epoch_days)); + *timep = (time_t)epoch_time; + if ((long long)*timep != epoch_time) { + goto overflow; + } + return 0; + +overflow: + errno = EOVERFLOW; + return -1; +} + +/** Parse some digits from a timestamp. */ +static int parse_timestamp_part(const char **str, size_t n, int *result) { + char buf[n + 1]; + for (size_t i = 0; i < n; ++i, ++*str) { + char c = **str; + if (c < '0' || c > '9') { + return -1; + } + buf[i] = c; + } + buf[n] = '\0'; + + *result = atoi(buf); + return 0; +} + +int parse_timestamp(const char *str, struct timespec *result) { + struct tm tm = { + .tm_isdst = -1, + }; + + int tz_hour = 0; + int tz_min = 0; + bool tz_negative = false; + bool local = true; + + // YYYY + if (parse_timestamp_part(&str, 4, &tm.tm_year) != 0) { + goto invalid; + } + tm.tm_year -= 1900; + + // MM + if (*str == '-') { + ++str; + } + if (parse_timestamp_part(&str, 2, &tm.tm_mon) != 0) { + goto invalid; + } + tm.tm_mon -= 1; + + // DD + if (*str == '-') { + ++str; + } + if (parse_timestamp_part(&str, 2, &tm.tm_mday) != 0) { + goto invalid; + } + + if (!*str) { + goto end; + } else if (*str == 'T') { + ++str; + } + + // hh + if (parse_timestamp_part(&str, 2, &tm.tm_hour) != 0) { + goto invalid; + } + + // mm + if (!*str) { + goto end; + } else if (*str == ':') { + ++str; + } + if (parse_timestamp_part(&str, 2, &tm.tm_min) != 0) { + goto invalid; + } + + // ss + if (!*str) { + goto end; + } else if (*str == ':') { + ++str; + } + if (parse_timestamp_part(&str, 2, &tm.tm_sec) != 0) { + goto invalid; + } + + if (!*str) { + goto end; + } else if (*str == 'Z') { + local = false; + ++str; + } else if (*str == '+' || *str == '-') { + local = false; + tz_negative = *str == '-'; + ++str; + + // hh + if (parse_timestamp_part(&str, 2, &tz_hour) != 0) { + goto invalid; + } + + // mm + if (!*str) { + goto end; + } else if (*str == ':') { + ++str; + } + if (parse_timestamp_part(&str, 2, &tz_min) != 0) { + goto invalid; + } + } else { + goto invalid; + } + + if (*str) { + goto invalid; + } + +end: + if (local) { + if (xmktime(&tm, &result->tv_sec) != 0) { + goto error; + } + } else { + if (xtimegm(&tm, &result->tv_sec) != 0) { + goto error; + } + + int offset = 60*tz_hour + tz_min; + if (tz_negative) { + result->tv_sec -= offset; + } else { + result->tv_sec += offset; + } + } + + result->tv_nsec = 0; + return 0; + +invalid: + errno = EINVAL; +error: + return -1; +} diff --git a/src/xtime.h b/src/xtime.h new file mode 100644 index 0000000..ceff48f --- /dev/null +++ b/src/xtime.h @@ -0,0 +1,86 @@ +/**************************************************************************** + * bfs * + * Copyright (C) 2020-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. * + ****************************************************************************/ + +/** + * Date/time handling. + */ + +#ifndef BFS_XTIME_H +#define BFS_XTIME_H + +#include <time.h> + +/** + * localtime_r() wrapper that calls tzset() first. + * + * @param[in] timep + * The time_t to convert. + * @param[out] result + * Buffer to hold the result. + * @return + * 0 on success, -1 on failure. + */ +int xlocaltime(const time_t *timep, struct tm *result); + +/** + * gmtime_r() wrapper that calls tzset() first. + * + * @param[in] timep + * The time_t to convert. + * @param[out] result + * Buffer to hold the result. + * @return + * 0 on success, -1 on failure. + */ +int xgmtime(const time_t *timep, struct tm *result); + +/** + * mktime() wrapper that reports errors more reliably. + * + * @param[in,out] tm + * The struct tm to convert. + * @param[out] timep + * A pointer to the result. + * @return + * 0 on success, -1 on failure. + */ +int xmktime(struct tm *tm, time_t *timep); + +/** + * A portable timegm(), the inverse of gmtime(). + * + * @param[in,out] tm + * The struct tm to convert. + * @param[out] timep + * A pointer to the result. + * @return + * 0 on success, -1 on failure. + */ +int xtimegm(struct tm *tm, time_t *timep); + +/** + * Parse an ISO 8601-style timestamp. + * + * @param[in] str + * The string to parse. + * @param[out] result + * A pointer to the result. + * @return + * 0 on success, -1 on failure. + */ +int parse_timestamp(const char *str, struct timespec *result); + +#endif // BFS_XTIME_H |