summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTavian Barnes <tavianator@tavianator.com>2019-06-28 20:34:33 -0400
committerTavian Barnes <tavianator@tavianator.com>2019-06-28 20:34:49 -0400
commite8b42e513fa97af5c9978eb95ea97712f0ea5bbb (patch)
treede9d429c0ca37e1cbff85f57a324564e1bfda76e
parent0473beaa2e3a46dd782af1182413fcdd4d33e275 (diff)
downloadbfs-single-file.tar.xz
Merge everything into one filesingle-file
-rw-r--r--Makefile19
-rw-r--r--bfs.c14265
-rw-r--r--bfs.h32
-rw-r--r--bftw.c1551
-rw-r--r--bftw.h238
-rw-r--r--cmdline.h132
-rw-r--r--color.c931
-rw-r--r--color.h124
-rw-r--r--diag.c64
-rw-r--r--diag.h60
-rw-r--r--dstring.c152
-rw-r--r--dstring.h130
-rw-r--r--eval.c1391
-rw-r--r--eval.h81
-rw-r--r--exec.c618
-rw-r--r--exec.h118
-rw-r--r--expr.h223
-rw-r--r--fsade.c297
-rw-r--r--fsade.h70
-rw-r--r--main.c111
-rw-r--r--mtab.c229
-rw-r--r--mtab.h70
-rw-r--r--opt.c870
-rw-r--r--parse.c3462
-rw-r--r--printf.c862
-rw-r--r--printf.h64
-rw-r--r--spawn.c226
-rw-r--r--spawn.h103
-rw-r--r--stat.c353
-rw-r--r--stat.h150
-rw-r--r--trie.c692
-rw-r--r--trie.h157
-rw-r--r--typo.c176
-rw-r--r--typo.h31
-rw-r--r--util.c383
-rw-r--r--util.h220
36 files changed, 14266 insertions, 14389 deletions
diff --git a/Makefile b/Makefile
index 6195eca..e8d62ca 100644
--- a/Makefile
+++ b/Makefile
@@ -68,24 +68,7 @@ default: bfs
all: bfs tests/mksock
-bfs: \
- bftw.o \
- color.o \
- diag.o \
- dstring.o \
- eval.o \
- exec.o \
- fsade.o \
- main.o \
- mtab.o \
- opt.o \
- parse.o \
- printf.o \
- spawn.o \
- stat.o \
- trie.o \
- typo.o \
- util.o
+bfs: bfs.o
$(CC) $(ALL_LDFLAGS) $^ $(ALL_LDLIBS) -o $@
release: CFLAGS := -g $(WFLAGS) -O3 -flto -DNDEBUG
diff --git a/bfs.c b/bfs.c
new file mode 100644
index 0000000..ce7cbb0
--- /dev/null
+++ b/bfs.c
@@ -0,0 +1,14265 @@
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2016-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. *
+ ****************************************************************************/
+
+/**
+ * Assorted utilities that don't belong anywhere else.
+ */
+
+#ifndef BFS_UTIL_H
+#define BFS_UTIL_H
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <time.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
+
+#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_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
+
+#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE)
+# define FNM_CASEFOLD FNM_IGNORECASE
+#endif
+
+#ifndef O_DIRECTORY
+# define O_DIRECTORY 0
+#endif
+
+/**
+ * Adds compiler warnings for bad printf()-style function calls, if supported.
+ */
+#if __GNUC__
+# define BFS_FORMATTER(fmt, args) __attribute__((format(printf, fmt, args)))
+#else
+# define BFS_FORMATTER(fmt, args)
+#endif
+
+/**
+ * readdir() wrapper that makes error handling cleaner.
+ */
+int xreaddir(DIR *dir, struct dirent **de);
+
+/**
+ * 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);
+
+/**
+ * Check if a file descriptor is open.
+ */
+bool isopen(int fd);
+
+/**
+ * Open a file and redirect it to a particular descriptor.
+ *
+ * @param fd
+ * The file descriptor to redirect.
+ * @param path
+ * The path to open.
+ * @param flags
+ * The flags passed to open().
+ * @param mode
+ * The mode passed to open() (optional).
+ * @return fd on success, -1 on failure.
+ */
+int redirect(int fd, const char *path, int flags, ...);
+
+/**
+ * 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]);
+
+/**
+ * Dynamically allocate a regex error message.
+ *
+ * @param err
+ * The error code to stringify.
+ * @param regex
+ * The (partially) compiled regex.
+ * @return A human-readable description of the error, allocated with malloc().
+ */
+char *xregerror(int err, const regex_t *regex);
+
+/**
+ * localtime_r() wrapper that calls tzset() first.
+ *
+ * @param timep
+ * The time_t to convert.
+ * @param result
+ * Buffer to hold the result.
+ * @return 0 on success, -1 on failure.
+ */
+int xlocaltime(const time_t *timep, struct tm *result);
+
+/**
+ * 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 format_mode(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);
+
+/**
+ * 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);
+
+#endif // BFS_UTIL_H
+/****************************************************************************
+ * 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 <sys/types.h>
+#include <sys/stat.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_ATIME = 1 << 10,
+ BFS_STAT_BTIME = 1 << 11,
+ BFS_STAT_CTIME = 1 << 12,
+ BFS_STAT_MTIME = 1 << 13,
+};
+
+/**
+ * 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_flag {
+ /** 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,
+};
+
+#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;
+
+ /** 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_flag 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
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-2018 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 <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+/**
+ * A lookup table for colors.
+ */
+struct colors;
+
+/**
+ * Parse a color table.
+ *
+ * @param ls_colors
+ * A color table in the LS_COLORS environment variable format.
+ * @return The parsed color table.
+ */
+struct colors *parse_colors(const char *ls_colors);
+
+/**
+ * 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;
+ /** Whether to close the underlying stream. */
+ bool close;
+} CFILE;
+
+/**
+ * Open a file for colored output.
+ *
+ * @param path
+ * The path to the file to open.
+ * @param colors
+ * The color table to use if file is a TTY.
+ * @return A colored file stream.
+ */
+CFILE *cfopen(const char *path, const struct colors *colors);
+
+/**
+ * Make a colored copy of an open file.
+ *
+ * @param file
+ * The underlying file.
+ * @param colors
+ * The color table to use if file is a TTY.
+ * @return A colored wrapper around file.
+ */
+CFILE *cfdup(FILE *file, const struct colors *colors);
+
+/**
+ * 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)
+ * %pP: A colored file path, from a const struct BFTW * argument
+ * %pL: A colored link target, from a const struct BFTW * argument
+ * %%: 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
+/****************************************************************************
+ * 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 <stdbool.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
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-2018 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 <regex.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <sys/types.h>
+#include <time.h>
+
+/**
+ * A command line expression.
+ */
+struct expr;
+
+/**
+ * Ephemeral state for evaluating an expression.
+ */
+struct eval_state;
+
+/**
+ * Expression evaluation function.
+ *
+ * @param expr
+ * The current expression.
+ * @param state
+ * The current evaluation state.
+ * @return
+ * The result of the test.
+ */
+typedef bool eval_fn(const struct expr *expr, struct eval_state *state);
+
+/**
+ * Possible types of numeric comparison.
+ */
+enum cmp_flag {
+ /** Exactly n. */
+ CMP_EXACT,
+ /** Less than n. */
+ CMP_LESS,
+ /** Greater than n. */
+ CMP_GREATER,
+};
+
+/**
+ * Possible types of mode comparison.
+ */
+enum mode_cmp {
+ /** Mode is an exact match (MODE). */
+ MODE_EXACT,
+ /** Mode has all these bits (-MODE). */
+ MODE_ALL,
+ /** Mode has any of these bits (/MODE). */
+ MODE_ANY,
+};
+
+/**
+ * Possible time units.
+ */
+enum time_unit {
+ /** Minutes. */
+ MINUTES,
+ /** Days. */
+ DAYS,
+};
+
+/**
+ * Possible file size units.
+ */
+enum size_unit {
+ /** 512-byte blocks. */
+ SIZE_BLOCKS,
+ /** Single bytes. */
+ SIZE_BYTES,
+ /** Two-byte words. */
+ SIZE_WORDS,
+ /** Kibibytes. */
+ SIZE_KB,
+ /** Mebibytes. */
+ SIZE_MB,
+ /** Gibibytes. */
+ SIZE_GB,
+ /** Tebibytes. */
+ SIZE_TB,
+ /** Pebibytes. */
+ SIZE_PB,
+};
+
+struct expr {
+ /** The function that evaluates this expression. */
+ eval_fn *eval;
+
+ /** The left hand side of the expression. */
+ struct expr *lhs;
+ /** The right hand side of the expression. */
+ struct expr *rhs;
+
+ /** 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;
+
+ /** Estimated cost. */
+ double cost;
+ /** Estimated probability of success. */
+ double probability;
+ /** Number of times this predicate was executed. */
+ size_t evaluations;
+ /** Number of times this predicate succeeded. */
+ size_t successes;
+ /** Total time spent running this predicate. */
+ struct timespec elapsed;
+
+ /** The number of command line arguments for this expression. */
+ size_t argc;
+ /** The command line arguments comprising this expression. */
+ char **argv;
+
+ /** The optional comparison flag. */
+ enum cmp_flag cmp_flag;
+
+ /** The mode comparison flag. */
+ enum 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;
+
+ /** The optional stat field to look at. */
+ enum bfs_stat_field stat_field;
+ /** The optional reference time. */
+ struct timespec reftime;
+ /** The optional time unit. */
+ enum time_unit time_unit;
+
+ /** The optional size unit. */
+ enum size_unit size_unit;
+
+ /** Optional device number for a target file. */
+ dev_t dev;
+ /** Optional inode number for a target file. */
+ ino_t ino;
+
+ /** File to output to. */
+ CFILE *cfile;
+
+ /** Optional compiled regex. */
+ regex_t *regex;
+
+ /** Optional exec command. */
+ struct bfs_exec *execbuf;
+
+ /** Optional printf command. */
+ struct bfs_printf *printf;
+
+ /** Optional integer data for this expression. */
+ long long idata;
+
+ /** Optional string data for this expression. */
+ const char *sdata;
+
+ /** 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;
+};
+
+/** Singleton true expression instance. */
+extern struct expr expr_true;
+/** Singleton false expression instance. */
+extern struct expr expr_false;
+
+/**
+ * Create a new expression.
+ */
+struct expr *new_expr(eval_fn *eval, size_t argc, char **argv);
+
+/**
+ * @return Whether expr is known to always quit.
+ */
+bool expr_never_returns(const struct expr *expr);
+
+/**
+ * @return The result of the comparison for this expression.
+ */
+bool expr_cmp(const struct expr *expr, long long n);
+
+/**
+ * Dump a parsed expression.
+ */
+void dump_expr(CFILE *cfile, const struct expr *expr, bool verbose);
+
+/**
+ * Free an expression tree.
+ */
+void free_expr(struct expr *expr);
+
+#endif // BFS_EXPR_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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. *
+ ****************************************************************************/
+
+/**
+ * Constants about the bfs program itself.
+ */
+
+#ifndef BFS_H
+#define BFS_H
+
+#ifndef BFS_VERSION
+# define BFS_VERSION "1.5"
+#endif
+
+#ifndef BFS_HOMEPAGE
+# define BFS_HOMEPAGE "https://github.com/tavianator/bfs"
+#endif
+
+#endif // BFS_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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 file-walking API based on nftw().
+ */
+
+#ifndef BFS_BFTW_H
+#define BFS_BFTW_H
+
+#include <stddef.h>
+#include <sys/types.h>
+
+/**
+ * Possible file types.
+ */
+enum bftw_typeflag {
+ /** Unknown type. */
+ BFTW_UNKNOWN = 0,
+ /** Block device. */
+ BFTW_BLK = 1 << 0,
+ /** Character device. */
+ BFTW_CHR = 1 << 1,
+ /** Directory. */
+ BFTW_DIR = 1 << 2,
+ /** Solaris door. */
+ BFTW_DOOR = 1 << 3,
+ /** Pipe. */
+ BFTW_FIFO = 1 << 4,
+ /** Symbolic link. */
+ BFTW_LNK = 1 << 5,
+ /** Solaris event port. */
+ BFTW_PORT = 1 << 6,
+ /** Regular file. */
+ BFTW_REG = 1 << 7,
+ /** Socket. */
+ BFTW_SOCK = 1 << 8,
+ /** BSD whiteout. */
+ BFTW_WHT = 1 << 9,
+ /** An error occurred for this file. */
+ BFTW_ERROR = 1 << 10,
+};
+
+/**
+ * Convert a bfs_stat() mode to a bftw() typeflag.
+ */
+enum bftw_typeflag bftw_mode_typeflag(mode_t mode);
+
+/**
+ * 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 bftw_typeflag typeflag;
+ /** The errno that occurred, if typeflag == 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_flag 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_flag 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 bftw_typeflag bftw_typeflag(const struct BFTW *ftwbuf, enum bfs_stat_flag 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_DEPTH = 1 << 2,
+ /** If the initial path is a symbolic link, follow it. */
+ BFTW_COMFOLLOW = 1 << 3,
+ /** Follow all symbolic links. */
+ BFTW_LOGICAL = 1 << 4,
+ /** Detect directory cycles. */
+ BFTW_DETECT_CYCLES = 1 << 5,
+ /** Stay on the same filesystem. */
+ BFTW_XDEV = 1 << 6,
+};
+
+/**
+ * Tree search strategies for bftw().
+ */
+enum bftw_strategy {
+ /** Breadth-first search. */
+ BFTW_BFS,
+ /** Depth-first search. */
+ BFTW_DFS,
+ /** Iterative deepening search. */
+ BFTW_IDS,
+};
+
+/**
+ * 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
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-2018 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. *
+ ****************************************************************************/
+
+/**
+ * Representation of the parsed command line.
+ */
+
+#ifndef BFS_CMDLINE_H
+#define BFS_CMDLINE_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,
+};
+
+/**
+ * The parsed command line.
+ */
+struct cmdline {
+ /** The unparsed command line arguments. */
+ char **argv;
+
+ /** The root paths. */
+ const char **paths;
+ /** The number of root paths. */
+ size_t npaths;
+
+ /** Color data. */
+ struct colors *colors;
+ /** Colored stdout. */
+ CFILE *cout;
+ /** Colored stderr. */
+ CFILE *cerr;
+
+ /** Table of mounted file systems. */
+ struct bfs_mtab *mtab;
+ /** The error that occurred parsing the mount table, if any. */
+ int mtab_error;
+
+ /** -mindepth option. */
+ int mindepth;
+ /** -maxdepth option. */
+ int maxdepth;
+
+ /** bftw() flags. */
+ enum bftw_flags flags;
+ /** bftw() search strategy. */
+ enum bftw_strategy strategy;
+
+ /** Optimization level. */
+ int optlevel;
+ /** Debugging flags. */
+ enum debug_flags debug;
+ /** Whether to only handle paths with xargs-safe characters. */
+ bool xargs_safe;
+ /** Whether to ignore deletions that race with bfs. */
+ bool ignore_races;
+ /** Whether to only return unique files. */
+ bool unique;
+
+ /** The command line expression. */
+ struct expr *expr;
+
+ /** All the open files owned by the command line. */
+ struct trie open_files;
+ /** The number of open files owned by the command line. */
+ int nopen_files;
+};
+
+/**
+ * Parse the command line.
+ */
+struct cmdline *parse_cmdline(int argc, char *argv[]);
+
+/**
+ * Dump the parsed command line.
+ */
+void dump_cmdline(const struct cmdline *cmdline, bool verbose);
+
+/**
+ * Optimize the parsed command line.
+ *
+ * @return 0 if successful, -1 on error.
+ */
+int optimize_cmdline(struct cmdline *cmdline);
+
+/**
+ * Evaluate the command line.
+ */
+int eval_cmdline(const struct cmdline *cmdline);
+
+/**
+ * Free the parsed command line.
+ *
+ * @return 0 if successful, -1 on error.
+ */
+int free_cmdline(struct cmdline *cmdline);
+
+#endif // BFS_CMDLINE_H
+/****************************************************************************
+ * 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. *
+ ****************************************************************************/
+
+/**
+ * Formatters for diagnostic messages.
+ */
+
+#ifndef BFS_DIAG_H
+#define BFS_DIAG_H
+
+#include <stdarg.h>
+
+/**
+ * Shorthand for printing error messages.
+ */
+BFS_FORMATTER(2, 3)
+void bfs_error(const struct cmdline *cmdline, const char *format, ...);
+
+/**
+ * Shorthand for printing warning messages.
+ */
+BFS_FORMATTER(2, 3)
+void bfs_warning(const struct cmdline *cmdline, const char *format, ...);
+
+/**
+ * bfs_error() variant that takes a va_list.
+ */
+void bfs_verror(const struct cmdline *cmdline, const char *format, va_list args);
+
+/**
+ * bfs_warning() variant that takes a va_list.
+ */
+void bfs_vwarning(const struct cmdline *cmdline, const char *format, va_list args);
+
+/**
+ * Print the error message prefix.
+ */
+void bfs_error_prefix(const struct cmdline *cmdline);
+
+/**
+ * Print the warning message prefix.
+ */
+void bfs_warning_prefix(const struct cmdline *cmdline);
+
+#endif // BFS_DIAG_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2016-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 dynamic string library.
+ */
+
+#ifndef BFS_DSTRING_H
+#define BFS_DSTRING_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);
+
+/**
+ * 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 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, ...);
+
+/**
+ * Free a dynamic string.
+ *
+ * @param dstr
+ * The string to free.
+ */
+void dstrfree(char *dstr);
+
+#endif // BFS_DSTRING_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-2018 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
+
+
+// Predicate evaluation functions
+bool eval_true(const struct expr *expr, struct eval_state *state);
+bool eval_false(const struct expr *expr, struct eval_state *state);
+
+bool eval_access(const struct expr *expr, struct eval_state *state);
+bool eval_acl(const struct expr *expr, struct eval_state *state);
+bool eval_capable(const struct expr *expr, struct eval_state *state);
+bool eval_perm(const struct expr *expr, struct eval_state *state);
+bool eval_xattr(const struct expr *expr, struct eval_state *state);
+
+bool eval_newer(const struct expr *expr, struct eval_state *state);
+bool eval_time(const struct expr *expr, struct eval_state *state);
+bool eval_used(const struct expr *expr, struct eval_state *state);
+
+bool eval_gid(const struct expr *expr, struct eval_state *state);
+bool eval_uid(const struct expr *expr, struct eval_state *state);
+bool eval_nogroup(const struct expr *expr, struct eval_state *state);
+bool eval_nouser(const struct expr *expr, struct eval_state *state);
+
+bool eval_depth(const struct expr *expr, struct eval_state *state);
+bool eval_empty(const struct expr *expr, struct eval_state *state);
+bool eval_fstype(const struct expr *expr, struct eval_state *state);
+bool eval_hidden(const struct expr *expr, struct eval_state *state);
+bool eval_inum(const struct expr *expr, struct eval_state *state);
+bool eval_links(const struct expr *expr, struct eval_state *state);
+bool eval_samefile(const struct expr *expr, struct eval_state *state);
+bool eval_size(const struct expr *expr, struct eval_state *state);
+bool eval_sparse(const struct expr *expr, struct eval_state *state);
+bool eval_type(const struct expr *expr, struct eval_state *state);
+bool eval_xtype(const struct expr *expr, struct eval_state *state);
+
+bool eval_lname(const struct expr *expr, struct eval_state *state);
+bool eval_name(const struct expr *expr, struct eval_state *state);
+bool eval_path(const struct expr *expr, struct eval_state *state);
+bool eval_regex(const struct expr *expr, struct eval_state *state);
+
+bool eval_delete(const struct expr *expr, struct eval_state *state);
+bool eval_exec(const struct expr *expr, struct eval_state *state);
+bool eval_exit(const struct expr *expr, struct eval_state *state);
+bool eval_nohidden(const struct expr *expr, struct eval_state *state);
+bool eval_fls(const struct expr *expr, struct eval_state *state);
+bool eval_fprint(const struct expr *expr, struct eval_state *state);
+bool eval_fprint0(const struct expr *expr, struct eval_state *state);
+bool eval_fprintf(const struct expr *expr, struct eval_state *state);
+bool eval_fprintx(const struct expr *expr, struct eval_state *state);
+bool eval_prune(const struct expr *expr, struct eval_state *state);
+bool eval_quit(const struct expr *expr, struct eval_state *state);
+
+// Operator evaluation functions
+bool eval_not(const struct expr *expr, struct eval_state *state);
+bool eval_and(const struct expr *expr, struct eval_state *state);
+bool eval_or(const struct expr *expr, struct eval_state *state);
+bool eval_comma(const struct expr *expr, struct eval_state *state);
+
+#endif // BFS_EVAL_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017 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
+
+
+struct cmdline;
+
+/**
+ * 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,
+ /** Print debugging information (-D exec). */
+ BFS_EXEC_DEBUG = 1 << 3,
+};
+
+/**
+ * Buffer for a command line to be executed.
+ */
+struct bfs_exec {
+ /** Flags for this exec buffer. */
+ enum bfs_exec_flags flags;
+
+ /** 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;
+
+ /** 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 cmdline
+ * The command line.
+ * @return The parsed exec action, or NULL on failure.
+ */
+struct bfs_exec *parse_bfs_exec(char **argv, enum bfs_exec_flags flags, const struct cmdline *cmdline);
+
+/**
+ * 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 free_bfs_exec(struct bfs_exec *execbuf);
+
+#endif // BFS_EXEC_H
+/****************************************************************************
+ * 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. *
+ ****************************************************************************/
+
+/**
+ * A facade over (file)system features that are (un)implemented differently
+ * between platforms.
+ */
+
+#ifndef BFS_FSADE_H
+#define BFS_FSADE_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_XATTR
+
+/**
+ * Check if a file has a non-trvial 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-trvial 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);
+
+#endif // BFS_FSADE_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017-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 platform-specific APIs for enumerating mounted filesystems.
+ */
+
+#ifndef BFS_MTAB_H
+#define BFS_MTAB_H
+
+#include <stdbool.h>
+
+/**
+ * A file system mount table.
+ */
+struct bfs_mtab;
+
+/**
+ * Parse the mount table.
+ *
+ * @return
+ * The parsed mount table, or NULL on error.
+ */
+struct bfs_mtab *parse_bfs_mtab(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_maybe_mount(const struct bfs_mtab *mtab, const char *path);
+
+/**
+ * Free a mount table.
+ */
+void free_bfs_mtab(struct bfs_mtab *mtab);
+
+#endif // BFS_MTAB_H
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017-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. *
+ ****************************************************************************/
+
+/**
+ * Implementation of -printf/-fprintf.
+ */
+
+#ifndef BFS_PRINTF_H
+#define BFS_PRINTF_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+/**
+ * A printf command, the result of parsing a single format string.
+ */
+struct bfs_printf;
+
+/**
+ * Parse a -printf format string.
+ *
+ * @param format
+ * The format string to parse.
+ * @param cmdline
+ * The command line.
+ * @return The parsed printf command, or NULL on failure.
+ */
+struct bfs_printf *parse_bfs_printf(const char *format, struct cmdline *cmdline);
+
+/**
+ * Evaluate a parsed format string.
+ *
+ * @param file
+ * The FILE to print to.
+ * @param command
+ * The parsed printf format.
+ * @param ftwbuf
+ * The bftw() data for the current file. If needs_stat is true, statbuf
+ * must be non-NULL.
+ * @return 0 on success, -1 on failure.
+ */
+int bfs_printf(FILE *file, const struct bfs_printf *command, const struct BFTW *ftwbuf);
+
+/**
+ * Free a parsed format string.
+ */
+void free_bfs_printf(struct bfs_printf *command);
+
+#endif // BFS_PRINTF_H
+/****************************************************************************
+ * 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 process-spawning library inspired by posix_spawn().
+ */
+
+#ifndef BFS_SPAWN_H
+#define BFS_SPAWN_H
+
+#include <stdbool.h>
+#include <signal.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);
+
+/**
+ * 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.
+ * @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);
+
+#endif // BFS_SPAWN_H
+/****************************************************************************
+ * 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
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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. *
+ ****************************************************************************/
+
+/**
+ * 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: Holds bftw_file's with open file descriptors, used for
+ * openat() to minimize the amount of path re-traversals that need to happen.
+ * Currently implemented as a priority queue based on depth and reference
+ * count.
+ *
+ * - struct bftw_queue: The queue of bftw_file's left to explore. Implemented
+ * as a simple circular buffer.
+ *
+ * - struct bftw_reader: A reader object that simplifies reading directories and
+ * reporting errors.
+ *
+ * - struct bftw_state: Represents the current state of the traversal, allowing
+ * various helper functions to take fewer parameters.
+ */
+
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/**
+ * A file.
+ */
+struct bftw_file {
+ /** The parent directory, if any. */
+ struct bftw_file *parent;
+ /** This file's depth in the walk. */
+ size_t depth;
+ /** The root path this file was found from. */
+ const char *root;
+
+ /** The next file in the queue, if any. */
+ struct bftw_file *next;
+
+ /** Reference count. */
+ size_t refcount;
+ /** Index in the bftw_cache priority queue. */
+ size_t heap_index;
+
+ /** An open descriptor to this file, or -1. */
+ int fd;
+
+ /** This file's type, if known. */
+ enum bftw_typeflag typeflag;
+ /** 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 {
+ /** A min-heap of open directories. */
+ struct bftw_file **heap;
+ /** Current heap size. */
+ size_t size;
+ /** Maximum heap size. */
+ size_t capacity;
+};
+
+/** Initialize a cache. */
+static int bftw_cache_init(struct bftw_cache *cache, size_t capacity) {
+ cache->heap = malloc(capacity*sizeof(*cache->heap));
+ if (!cache->heap) {
+ return -1;
+ }
+
+ cache->size = 0;
+ cache->capacity = capacity;
+ return 0;
+}
+
+/** Destroy a cache. */
+static void bftw_cache_destroy(struct bftw_cache *cache) {
+ assert(cache->size == 0);
+ free(cache->heap);
+}
+
+/** Check if two heap entries are in heap order. */
+static bool bftw_heap_check(const struct bftw_file *parent, const struct bftw_file *child) {
+ if (parent->depth > child->depth) {
+ return true;
+ } else if (parent->depth < child->depth) {
+ return false;
+ } else {
+ return parent->refcount <= child->refcount;
+ }
+}
+
+/** Move a bftw_file to a particular place in the heap. */
+static void bftw_heap_move(struct bftw_cache *cache, struct bftw_file *file, size_t i) {
+ cache->heap[i] = file;
+ file->heap_index = i;
+}
+
+/** Bubble an entry up the heap. */
+static void bftw_heap_bubble_up(struct bftw_cache *cache, struct bftw_file *file) {
+ size_t i = file->heap_index;
+
+ while (i > 0) {
+ size_t pi = (i - 1)/2;
+ struct bftw_file *parent = cache->heap[pi];
+ if (bftw_heap_check(parent, file)) {
+ break;
+ }
+
+ bftw_heap_move(cache, parent, i);
+ i = pi;
+ }
+
+ bftw_heap_move(cache, file, i);
+}
+
+/** Bubble an entry down the heap. */
+static void bftw_heap_bubble_down(struct bftw_cache *cache, struct bftw_file *file) {
+ size_t i = file->heap_index;
+
+ while (true) {
+ size_t ci = 2*i + 1;
+ if (ci >= cache->size) {
+ break;
+ }
+
+ struct bftw_file *child = cache->heap[ci];
+
+ size_t ri = ci + 1;
+ if (ri < cache->size) {
+ struct bftw_file *right = cache->heap[ri];
+ if (!bftw_heap_check(child, right)) {
+ ci = ri;
+ child = right;
+ }
+ }
+
+ if (bftw_heap_check(file, child)) {
+ break;
+ }
+
+ bftw_heap_move(cache, child, i);
+ i = ci;
+ }
+
+ bftw_heap_move(cache, file, i);
+}
+
+/** Bubble an entry up or down the heap. */
+static void bftw_heap_bubble(struct bftw_cache *cache, struct bftw_file *file) {
+ size_t i = file->heap_index;
+
+ if (i > 0) {
+ size_t pi = (i - 1)/2;
+ struct bftw_file *parent = cache->heap[pi];
+ if (!bftw_heap_check(parent, file)) {
+ bftw_heap_bubble_up(cache, file);
+ return;
+ }
+ }
+
+ bftw_heap_bubble_down(cache, file);
+}
+
+/** Increment a bftw_file's reference count. */
+static size_t bftw_file_incref(struct bftw_cache *cache, struct bftw_file *file) {
+ size_t ret = ++file->refcount;
+ if (file->fd >= 0) {
+ bftw_heap_bubble_down(cache, file);
+ }
+ return ret;
+}
+
+/** Decrement a bftw_file's reference count. */
+static size_t bftw_file_decref(struct bftw_cache *cache, struct bftw_file *file) {
+ size_t ret = --file->refcount;
+ if (file->fd >= 0) {
+ bftw_heap_bubble_up(cache, file);
+ }
+ return ret;
+}
+
+/** Add a bftw_file to the cache. */
+static void bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) {
+ assert(cache->size < cache->capacity);
+ assert(file->fd >= 0);
+
+ size_t size = cache->size++;
+ file->heap_index = size;
+ bftw_heap_bubble_up(cache, file);
+}
+
+/** Remove a bftw_file from the cache. */
+static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) {
+ assert(cache->size > 0);
+ assert(file->fd >= 0);
+
+ size_t size = --cache->size;
+ size_t i = file->heap_index;
+ if (i != size) {
+ struct bftw_file *end = cache->heap[size];
+ end->heap_index = i;
+ bftw_heap_bubble(cache, end);
+ }
+}
+
+/** 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);
+
+ close(file->fd);
+ file->fd = -1;
+}
+
+/** Pop a directory from the cache. */
+static void bftw_cache_pop(struct bftw_cache *cache) {
+ assert(cache->size > 0);
+ bftw_file_close(cache, cache->heap[0]);
+}
+
+/**
+ * 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) {
+ int ret = -1;
+ struct bftw_file *file = NULL;
+
+ if (cache->size >= 1) {
+ file = cache->heap[0];
+ if (file == saved && cache->size >= 2) {
+ file = cache->heap[1];
+ }
+ }
+
+ if (file && file != saved) {
+ bftw_file_close(cache, file);
+ ret = 0;
+ }
+
+ cache->capacity = cache->size;
+ return ret;
+}
+
+/** 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_cache *cache, struct bftw_file *parent, const char *name) {
+ size_t namelen = strlen(name);
+ size_t size = sizeof(struct bftw_file) + namelen + 1;
+
+ struct bftw_file *file = malloc(size);
+ if (!file) {
+ return NULL;
+ }
+
+ file->parent = parent;
+
+ if (parent) {
+ file->depth = parent->depth + 1;
+ file->root = parent->root;
+ file->nameoff = bftw_child_nameoff(parent);
+ bftw_file_incref(cache, parent);
+ } else {
+ file->depth = 0;
+ file->root = name;
+ file->nameoff = 0;
+ }
+
+ file->next = NULL;
+
+ file->refcount = 1;
+ file->fd = -1;
+
+ file->typeflag = BFTW_UNKNOWN;
+ file->dev = -1;
+ file->ino = -1;
+
+ file->namelen = namelen;
+ memcpy(file->name, name, namelen + 1);
+
+ return file;
+}
+
+/**
+ * Get the appropriate (fd, path) pair for the *at() family of functions.
+ *
+ * @param file
+ * The file being accessed.
+ * @param[out] at_fd
+ * Will hold the appropriate file descriptor to use.
+ * @param[in,out] at_path
+ * Will hold the appropriate path to use.
+ * @return The closest open ancestor file.
+ */
+static struct bftw_file *bftw_file_base(struct bftw_file *file, int *at_fd, const char **at_path) {
+ struct bftw_file *base = file;
+
+ do {
+ base = base->parent;
+ } while (base && base->fd < 0);
+
+ if (base) {
+ *at_fd = base->fd;
+ *at_path += bftw_child_nameoff(base);
+ }
+
+ return base;
+}
+
+/**
+ * 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, const struct bftw_file *base, int at_fd, const char *at_path) {
+ assert(file->fd < 0);
+
+ 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(base->fd, at_path, flags);
+ }
+ }
+
+ if (fd >= 0) {
+ if (cache->size == cache->capacity) {
+ 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) {
+ int at_fd = AT_FDCWD;
+ const char *at_path = path;
+ struct bftw_file *base = bftw_file_base(file, &at_fd, &at_path);
+
+ int fd = bftw_file_openat(cache, file, base, at_fd, at_path);
+ if (fd >= 0 || errno != ENAMETOOLONG) {
+ return fd;
+ }
+
+ // Handle ENAMETOOLONG by manually traversing the path component-by-component
+
+ // -1 to include the root, which has depth == 0
+ size_t offset = base ? base->depth : -1;
+ size_t levels = file->depth - offset;
+ if (levels < 2) {
+ return fd;
+ }
+
+ struct bftw_file **parents = malloc(levels * sizeof(*parents));
+ if (!parents) {
+ return fd;
+ }
+
+ struct bftw_file *parent = file;
+ for (size_t i = levels; i-- > 0;) {
+ parents[i] = parent;
+ parent = parent->parent;
+ }
+
+ for (size_t i = 0; i < levels; ++i) {
+ fd = bftw_file_openat(cache, parents[i], base, at_fd, parents[i]->name);
+ if (fd < 0) {
+ break;
+ }
+
+ base = parents[i];
+ at_fd = fd;
+ }
+
+ free(parents);
+ return fd;
+}
+
+/**
+ * Open a DIR* for a bftw_file.
+ *
+ * @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 DIR *, or NULL on error.
+ */
+static 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;
+ }
+
+ // Now we dup() the fd and pass it to fdopendir(). This way we can
+ // close the DIR* as soon as we're done with it, reducing the memory
+ // footprint significantly, while keeping the fd around for future
+ // openat() calls.
+
+ int dfd = dup_cloexec(fd);
+
+ if (dfd < 0 && errno == EMFILE) {
+ if (bftw_cache_shrink(cache, file) == 0) {
+ dfd = dup_cloexec(fd);
+ }
+ }
+
+ if (dfd < 0) {
+ return NULL;
+ }
+
+ DIR *ret = fdopendir(dfd);
+ if (!ret) {
+ int error = errno;
+ close(dfd);
+ errno = error;
+ }
+
+ return ret;
+}
+
+/** 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 tail of the queue. */
+ struct bftw_file *tail;
+};
+
+/** Initialize a bftw_queue. */
+static void bftw_queue_init(struct bftw_queue *queue) {
+ queue->head = NULL;
+ queue->tail = NULL;
+}
+
+/** Add a file to the tail of the bftw_queue. */
+static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) {
+ assert(file->next == NULL);
+
+ if (!queue->head) {
+ queue->head = file;
+ }
+ if (queue->tail) {
+ queue->tail->next = file;
+ }
+ queue->tail = file;
+}
+
+/** Prepend a queue to the head of another one. */
+static void bftw_queue_prepend(struct bftw_queue *head, struct bftw_queue *tail) {
+ if (head->tail) {
+ head->tail->next = tail->head;
+ }
+ if (head->head) {
+ tail->head = head->head;
+ }
+ if (!tail->tail) {
+ tail->tail = head->tail;
+ }
+ head->head = NULL;
+ head->tail = NULL;
+}
+
+/** 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;
+ if (queue->tail == file) {
+ queue->tail = NULL;
+ }
+ file->next = NULL;
+ return file;
+}
+
+/**
+ * A directory reader.
+ */
+struct bftw_reader {
+ /** The open handle to the directory. */
+ DIR *dir;
+ /** The current directory entry. */
+ struct dirent *de;
+ /** Any error code that has occurred. */
+ int error;
+};
+
+/** Initialize a reader. */
+static void bftw_reader_init(struct bftw_reader *reader) {
+ reader->dir = NULL;
+ reader->de = NULL;
+ reader->error = 0;
+}
+
+/** Open a directory for reading. */
+static int bftw_reader_open(struct bftw_reader *reader, struct bftw_cache *cache, struct bftw_file *file, const char *path) {
+ assert(!reader->dir);
+ assert(!reader->de);
+
+ reader->error = 0;
+
+ reader->dir = bftw_file_opendir(cache, file, path);
+ if (!reader->dir) {
+ reader->error = errno;
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Read a directory entry. */
+static int bftw_reader_read(struct bftw_reader *reader) {
+ if (!reader->dir) {
+ return -1;
+ }
+
+ if (xreaddir(reader->dir, &reader->de) != 0) {
+ reader->error = errno;
+ return -1;
+ } else if (reader->de) {
+ return 1;
+ } else {
+ return 0;
+ }
+}
+
+/** Close a directory. */
+static int bftw_reader_close(struct bftw_reader *reader) {
+ int ret = 0;
+ if (reader->dir && closedir(reader->dir) != 0) {
+ reader->error = errno;
+ ret = -1;
+ }
+
+ reader->de = NULL;
+ reader->dir = NULL;
+ return ret;
+}
+
+/**
+ * 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;
+ /** An intermediate queue used for depth-first searches. */
+ struct bftw_queue prequeue;
+
+ /** The current path. */
+ char *path;
+ /** The current file. */
+ struct bftw_file *file;
+ /** The previous file. */
+ struct bftw_file *previous;
+ /** The reader for the current directory. */
+ struct bftw_reader reader;
+
+ /** 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 < 2) {
+ errno = EMFILE;
+ goto err;
+ }
+
+ // Reserve 1 fd for the open DIR *
+ if (bftw_cache_init(&state->cache, args->nopenfd - 1) != 0) {
+ goto err;
+ }
+
+ bftw_queue_init(&state->queue);
+ bftw_queue_init(&state->prequeue);
+
+ state->path = dstralloc(0);
+ if (!state->path) {
+ goto err_cache;
+ }
+
+ state->file = NULL;
+ state->previous = NULL;
+ bftw_reader_init(&state->reader);
+
+ return 0;
+
+err_cache:
+ bftw_cache_destroy(&state->cache);
+err:
+ return -1;
+}
+
+enum bftw_typeflag bftw_mode_typeflag(mode_t mode) {
+ switch (mode & S_IFMT) {
+#ifdef S_IFBLK
+ case S_IFBLK:
+ return BFTW_BLK;
+#endif
+#ifdef S_IFCHR
+ case S_IFCHR:
+ return BFTW_CHR;
+#endif
+#ifdef S_IFDIR
+ case S_IFDIR:
+ return BFTW_DIR;
+#endif
+#ifdef S_IFDOOR
+ case S_IFDOOR:
+ return BFTW_DOOR;
+#endif
+#ifdef S_IFIFO
+ case S_IFIFO:
+ return BFTW_FIFO;
+#endif
+#ifdef S_IFLNK
+ case S_IFLNK:
+ return BFTW_LNK;
+#endif
+#ifdef S_IFPORT
+ case S_IFPORT:
+ return BFTW_PORT;
+#endif
+#ifdef S_IFREG
+ case S_IFREG:
+ return BFTW_REG;
+#endif
+#ifdef S_IFSOCK
+ case S_IFSOCK:
+ return BFTW_SOCK;
+#endif
+#ifdef S_IFWHT
+ case S_IFWHT:
+ return BFTW_WHT;
+#endif
+
+ default:
+ return BFTW_UNKNOWN;
+ }
+}
+
+static enum bftw_typeflag bftw_dirent_typeflag(const struct dirent *de) {
+#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN)
+ switch (de->d_type) {
+#ifdef DT_BLK
+ case DT_BLK:
+ return BFTW_BLK;
+#endif
+#ifdef DT_CHR
+ case DT_CHR:
+ return BFTW_CHR;
+#endif
+#ifdef DT_DIR
+ case DT_DIR:
+ return BFTW_DIR;
+#endif
+#ifdef DT_DOOR
+ case DT_DOOR:
+ return BFTW_DOOR;
+#endif
+#ifdef DT_FIFO
+ case DT_FIFO:
+ return BFTW_FIFO;
+#endif
+#ifdef DT_LNK
+ case DT_LNK:
+ return BFTW_LNK;
+#endif
+#ifdef DT_PORT
+ case DT_PORT:
+ return BFTW_PORT;
+#endif
+#ifdef DT_REG
+ case DT_REG:
+ return BFTW_REG;
+#endif
+#ifdef DT_SOCK
+ case DT_SOCK:
+ return BFTW_SOCK;
+#endif
+#ifdef DT_WHT
+ case DT_WHT:
+ return BFTW_WHT;
+#endif
+ }
+#endif
+
+ return BFTW_UNKNOWN;
+}
+
+/** Cached bfs_stat(). */
+static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, struct bftw_stat *cache, enum bfs_stat_flag 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_flag 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;
+}
+
+enum bftw_typeflag bftw_typeflag(const struct BFTW *ftwbuf, enum bfs_stat_flag flags) {
+ if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) {
+ if ((flags & BFS_STAT_NOFOLLOW) || ftwbuf->typeflag != BFTW_LNK) {
+ return ftwbuf->typeflag;
+ }
+ } else if ((flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW || ftwbuf->typeflag == BFTW_LNK) {
+ return ftwbuf->typeflag;
+ }
+
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags);
+ if (statbuf) {
+ return bftw_mode_typeflag(statbuf->mode);
+ } else {
+ return BFTW_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->typeflag == BFTW_UNKNOWN) {
+ return true;
+ }
+
+ if (ftwbuf->typeflag == BFTW_LNK && !(ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
+ return true;
+ }
+
+ if (ftwbuf->typeflag == BFTW_DIR) {
+ if (state->flags & (BFTW_DETECT_CYCLES | BFTW_XDEV)) {
+ 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_maybe_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 bftw_reader *reader = &state->reader;
+ const struct dirent *de = reader->de;
+
+ struct BFTW *ftwbuf = &state->ftwbuf;
+ ftwbuf->path = state->path;
+ ftwbuf->root = file ? file->root : ftwbuf->path;
+ ftwbuf->depth = 0;
+ ftwbuf->visit = visit;
+ ftwbuf->typeflag = BFTW_UNKNOWN;
+ ftwbuf->error = reader->error;
+ 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->typeflag = bftw_dirent_typeflag(de);
+ ftwbuf->nameoff = bftw_child_nameoff(file);
+ } else if (file) {
+ parent = file->parent;
+ ftwbuf->depth = file->depth;
+ ftwbuf->typeflag = file->typeflag;
+ 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->typeflag = BFTW_ERROR;
+ return;
+ }
+
+ int follow_flags = BFTW_LOGICAL;
+ if (ftwbuf->depth == 0) {
+ follow_flags |= BFTW_COMFOLLOW;
+ }
+ 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->typeflag = bftw_mode_typeflag(statbuf->mode);
+ } else {
+ ftwbuf->typeflag = BFTW_ERROR;
+ ftwbuf->error = errno;
+ return;
+ }
+ }
+
+ if (ftwbuf->typeflag == BFTW_DIR && (state->flags & BFTW_DETECT_CYCLES)) {
+ for (const struct bftw_file *parent = file; parent; parent = parent->parent) {
+ if (parent->depth == ftwbuf->depth) {
+ continue;
+ }
+ if (parent->dev == statbuf->dev && parent->ino == statbuf->ino) {
+ ftwbuf->typeflag = BFTW_ERROR;
+ ftwbuf->error = ELOOP;
+ return;
+ }
+ }
+ }
+}
+
+/** 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 BFTW_ERROR unless BFTW_RECOVER is specified
+ if (ftwbuf->typeflag == BFTW_ERROR && !(state->flags & BFTW_RECOVER)) {
+ state->error = ftwbuf->error;
+ return BFTW_STOP;
+ }
+
+ 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->typeflag != BFTW_DIR) {
+ ret = BFTW_PRUNE;
+ goto done;
+ }
+
+ if (state->flags & BFTW_XDEV) {
+ const struct bftw_file *parent = state->file;
+ if (parent && !name) {
+ parent = parent->parent;
+ }
+
+ if (parent) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (statbuf && statbuf->dev != parent->dev) {
+ 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(&state->cache, parent, name);
+ if (!file) {
+ state->error = errno;
+ return -1;
+ }
+
+ struct dirent *de = state->reader.de;
+ if (de) {
+ file->typeflag = bftw_dirent_typeflag(de);
+ }
+
+ if (fill_id) {
+ bftw_fill_id(file, &state->ftwbuf);
+ }
+
+ if (state->strategy == BFTW_DFS) {
+ bftw_queue_push(&state->prequeue, file);
+ } else {
+ 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->strategy == BFTW_DFS) {
+ bftw_queue_prepend(&state->prequeue, &state->queue);
+ }
+
+ if (!state->queue.head) {
+ return 0;
+ }
+
+ state->file = bftw_queue_pop(&state->queue);
+
+ if (bftw_build_path(state) != 0) {
+ return -1;
+ }
+
+ return 1;
+}
+
+/**
+ * Open a reader for the current directory.
+ */
+static struct bftw_reader *bftw_open(struct bftw_state *state) {
+ struct bftw_reader *reader = &state->reader;
+ bftw_reader_open(reader, &state->cache, state->file, state->path);
+ return reader;
+}
+
+/**
+ * Close and release the reader.
+ */
+static enum bftw_action bftw_release_reader(struct bftw_state *state, bool do_visit) {
+ enum bftw_action ret = BFTW_CONTINUE;
+
+ struct bftw_reader *reader = &state->reader;
+ bftw_reader_close(reader);
+
+ if (reader->error != 0) {
+ if (do_visit) {
+ if (bftw_visit(state, NULL, BFTW_PRE) == BFTW_STOP) {
+ ret = BFTW_STOP;
+ }
+ } else {
+ state->error = reader->error;
+ }
+ reader->error = 0;
+ }
+
+ return ret;
+}
+
+/**
+ * Finalize and free a file we're done with.
+ */
+static enum bftw_action bftw_release_file(struct bftw_state *state, bool visit_file, bool visit_parents) {
+ enum bftw_action ret = BFTW_CONTINUE;
+
+ if (!(state->flags & BFTW_DEPTH)) {
+ visit_file = false;
+ visit_parents = false;
+ }
+ bool do_visit = visit_file;
+
+ while (state->file) {
+ if (bftw_file_decref(&state->cache, state->file) > 0) {
+ state->file = NULL;
+ break;
+ }
+
+ if (do_visit) {
+ if (bftw_visit(state, NULL, BFTW_POST) == BFTW_STOP) {
+ ret = BFTW_STOP;
+ visit_parents = false;
+ }
+ }
+ do_visit = visit_parents;
+
+ struct bftw_file *parent = state->file->parent;
+ if (state->previous == state->file) {
+ state->previous = parent;
+ }
+ bftw_file_free(&state->cache, state->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_release_file(state, false, false);
+ }
+}
+
+/**
+ * Dispose of the bftw() state.
+ *
+ * @return
+ * The bftw() return value.
+ */
+static int bftw_state_destroy(struct bftw_state *state) {
+ dstrfree(state->path);
+
+ bftw_release_reader(state, false);
+
+ bftw_release_file(state, false, false);
+ bftw_drain_queue(state, &state->prequeue);
+ bftw_drain_queue(state, &state->queue);
+
+ bftw_cache_destroy(&state->cache);
+
+ errno = state->error;
+ return state->error ? -1 : 0;
+}
+
+/**
+ * Breadth-first bftw() implementation.
+ */
+static int bftw_bfs(const struct bftw_args *args) {
+ struct bftw_state state;
+ if (bftw_state_init(&state, args) != 0) {
+ return -1;
+ }
+
+ 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;
+ }
+ }
+
+ while (bftw_pop(&state) > 0) {
+ struct bftw_reader *reader = bftw_open(&state);
+
+ while (bftw_reader_read(reader) > 0) {
+ const char *name = reader->de->d_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;
+ }
+ }
+
+ if (bftw_release_reader(&state, true) == BFTW_STOP) {
+ goto done;
+ }
+ if (bftw_release_file(&state, true, true) == BFTW_STOP) {
+ goto done;
+ }
+ }
+
+done:
+ return bftw_state_destroy(&state);
+}
+
+/**
+ * Depth-first bftw() implementation.
+ */
+static int bftw_dfs(const struct bftw_args *args) {
+ struct bftw_state state;
+ if (bftw_state_init(&state, args) != 0) {
+ return -1;
+ }
+
+ for (size_t i = 0; i < args->npaths; ++i) {
+ if (bftw_push(&state, args->paths[i], false) != 0) {
+ goto done;
+ }
+ }
+
+ while (bftw_pop(&state) > 0) {
+ bool visit_post = true;
+
+ switch (bftw_visit(&state, NULL, BFTW_PRE)) {
+ case BFTW_CONTINUE:
+ break;
+ case BFTW_PRUNE:
+ visit_post = false;
+ goto next;
+ case BFTW_STOP:
+ goto done;
+ }
+
+ struct bftw_reader *reader = bftw_open(&state);
+
+ while (bftw_reader_read(reader) > 0) {
+ if (bftw_push(&state, reader->de->d_name, false) != 0) {
+ goto done;
+ }
+ }
+
+ if (bftw_release_reader(&state, true) == BFTW_STOP) {
+ goto done;
+ }
+
+ next:
+ if (bftw_release_file(&state, visit_post, true) == BFTW_STOP) {
+ goto done;
+ }
+ }
+
+done:
+ return bftw_state_destroy(&state);
+}
+
+/**
+ * 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;
+ /** The current target depth. */
+ size_t 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;
+
+ struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
+ mutbuf->visit = state->visit;
+
+ if (ftwbuf->typeflag == BFTW_ERROR) {
+ if (state->depth - ftwbuf->depth <= 1) {
+ return state->delegate(ftwbuf, state->ptr);
+ } else {
+ return BFTW_PRUNE;
+ }
+ }
+
+ if (ftwbuf->depth < state->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;
+ }
+ }
+
+ state->bottom = false;
+
+ enum bftw_action ret = state->delegate(ftwbuf, state->ptr);
+
+ switch (ret) {
+ case BFTW_CONTINUE:
+ ret = BFTW_PRUNE;
+ break;
+ case BFTW_PRUNE:
+ if (ftwbuf->typeflag == BFTW_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;
+}
+
+/**
+ * Iterative deepening bftw() wrapper.
+ */
+static int bftw_ids(const struct bftw_args *args) {
+ struct trie pruned;
+ trie_init(&pruned);
+
+ struct bftw_ids_state state = {
+ .delegate = args->callback,
+ .ptr = args->ptr,
+ .visit = BFTW_PRE,
+ .depth = 0,
+ .pruned = &pruned,
+ .bottom = false,
+ };
+
+ struct bftw_args ids_args = *args;
+ ids_args.callback = bftw_ids_callback;
+ ids_args.ptr = &state;
+ ids_args.flags &= ~BFTW_DEPTH;
+ ids_args.strategy = BFTW_DFS;
+
+ int ret = 0;
+
+ while (ret == 0 && !state.quit && !state.bottom) {
+ state.bottom = true;
+
+ // bftw_bfs() is more efficient than bftw_dfs() since it visits
+ // directory entries as it reads them. With args->strategy ==
+ // BFTW_DFS, it gives a hybrid ordering that visits immediate
+ // children first, then deeper descendants depth-first. This
+ // doesn't matter for iterative deepening since we only visit
+ // one level at a time.
+ ret = bftw_bfs(&ids_args);
+
+ ++state.depth;
+ }
+
+ if (args->flags & BFTW_DEPTH) {
+ state.visit = BFTW_POST;
+
+ while (ret == 0 && !state.quit && state.depth > 0) {
+ --state.depth;
+ ret = bftw_bfs(&ids_args);
+ }
+ }
+
+ if (state.error) {
+ ret = -1;
+ } else {
+ state.error = errno;
+ }
+ trie_destroy(&pruned);
+ errno = state.error;
+ return ret;
+}
+
+int bftw(const struct bftw_args *args) {
+ switch (args->strategy) {
+ case BFTW_BFS:
+ return bftw_bfs(args);
+ case BFTW_DFS:
+ return bftw_dfs(args);
+ case BFTW_IDS:
+ return bftw_ids(args);
+ }
+
+ errno = EINVAL;
+ return -1;
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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 <assert.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+/**
+ * The parsed form of LS_COLORS.
+ */
+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 void set_color(struct colors *colors, const char *name, char *value) {
+ char **color = get_color(colors, name);
+ if (color) {
+ dstrfree(*color);
+ *color = value;
+ }
+}
+
+/**
+ * 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;
+}
+
+struct colors *parse_colors(const char *ls_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", &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, "wr", "01;33", &colors->warning);
+ ret |= init_color(colors, "er", "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;
+ }
+
+ 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;
+ }
+
+ set_color(colors, key, value);
+ free(key);
+ }
+ }
+
+ if (colors->link && strcmp(colors->link, "target") == 0) {
+ colors->link_as_target = true;
+ dstrfree(colors->link);
+ colors->link = NULL;
+ }
+
+ 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 *cfopen(const char *path, const struct colors *colors) {
+ CFILE *cfile = malloc(sizeof(*cfile));
+ if (!cfile) {
+ return NULL;
+ }
+
+ cfile->close = false;
+ cfile->file = fopen(path, "wb");
+ if (!cfile->file) {
+ cfclose(cfile);
+ return NULL;
+ }
+ cfile->close = true;
+
+ if (isatty(fileno(cfile->file))) {
+ cfile->colors = colors;
+ } else {
+ cfile->colors = NULL;
+ }
+
+ return cfile;
+}
+
+CFILE *cfdup(FILE *file, const struct colors *colors) {
+ CFILE *cfile = malloc(sizeof(*cfile));
+ if (!cfile) {
+ return NULL;
+ }
+
+ cfile->close = false;
+ cfile->file = file;
+
+ if (isatty(fileno(file))) {
+ cfile->colors = colors;
+ } else {
+ cfile->colors = NULL;
+ }
+
+ return cfile;
+}
+
+int cfclose(CFILE *cfile) {
+ int ret = 0;
+ if (cfile) {
+ 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_flag flags) {
+ enum bftw_typeflag typeflag = bftw_typeflag(ftwbuf, flags);
+ if (typeflag == BFTW_ERROR) {
+ goto error;
+ }
+
+ const struct bfs_stat *statbuf = NULL;
+ const char *color = NULL;
+
+ switch (typeflag) {
+ case BFTW_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 BFTW_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 BFTW_LNK:
+ if (colors->orphan && is_link_broken(ftwbuf)) {
+ color = colors->orphan;
+ } else {
+ color = colors->link;
+ }
+ break;
+
+ case BFTW_BLK:
+ color = colors->blockdev;
+ break;
+ case BFTW_CHR:
+ color = colors->chardev;
+ break;
+ case BFTW_FIFO:
+ color = colors->pipe;
+ break;
+ case BFTW_SOCK:
+ color = colors->socket;
+ break;
+ case BFTW_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 a fixed-length string. */
+static int print_strn(const char *str, size_t len, FILE *file) {
+ if (fwrite(str, 1, len, file) == len) {
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+/** Print a dstring. */
+static int print_dstr(const char *str, FILE *file) {
+ return print_strn(str, dstrlen(str), file);
+}
+
+/** Print an ANSI escape sequence. */
+static int print_esc(const struct colors *colors, const char *esc, FILE *file) {
+ if (print_dstr(colors->leftcode, file) != 0) {
+ return -1;
+ }
+ if (print_dstr(esc, file) != 0) {
+ return -1;
+ }
+ if (print_dstr(colors->rightcode, file) != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Reset after an ANSI escape sequence. */
+static int print_reset(const struct colors *colors, FILE *file) {
+ if (colors->endcode) {
+ return print_dstr(colors->endcode, file);
+ } else {
+ return print_esc(colors, colors->reset, file);
+ }
+}
+
+/** Print a string with an optional color. */
+static int print_colored(const struct colors *colors, const char *esc, const char *str, size_t len, FILE *file) {
+ if (esc) {
+ if (print_esc(colors, esc, file) != 0) {
+ return -1;
+ }
+ }
+ if (print_strn(str, len, file) != 0) {
+ return -1;
+ }
+ if (esc) {
+ if (print_reset(colors, file) != 0) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/** Print a path with colors. */
+static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flag flags) {
+ const struct colors *colors = cfile->colors;
+ FILE *file = cfile->file;
+
+ size_t nameoff;
+ if (path == ftwbuf->path) {
+ nameoff = ftwbuf->nameoff;
+ } else {
+ nameoff = xbasename(path) - path;
+ }
+
+ if (nameoff > 0) {
+ if (print_colored(colors, colors->directory, path, nameoff, file) != 0) {
+ return -1;
+ }
+ }
+
+ const char *filename = path + nameoff;
+ const char *color = file_color(colors, filename, ftwbuf, flags);
+ return print_colored(colors, color, filename, strlen(filename), file);
+}
+
+/** 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 fputs(ftwbuf->path, cfile->file) == EOF ? -1 : 0;
+ }
+
+ enum bfs_stat_flag flags = ftwbuf->stat_flags;
+ if (colors && colors->link_as_target && ftwbuf->typeflag == BFTW_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) {
+ int ret = -1;
+
+ size_t len = 0;
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_NOFOLLOW);
+ if (statbuf) {
+ len = statbuf->size;
+ }
+
+ char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
+ if (!target) {
+ goto done;
+ }
+
+ if (!cfile->colors) {
+ ret = fputs(target, cfile->file) == EOF ? -1 : 0;
+ goto done;
+ }
+
+ ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW);
+
+done:
+ free(target);
+ 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;
+}
+
+int cvfprintf(CFILE *cfile, const char *format, va_list args) {
+ const struct colors *colors = cfile->colors;
+ FILE *file = cfile->file;
+ int error = errno;
+
+ for (const char *i = format; *i; ++i) {
+ size_t verbatim = strcspn(i, "%$");
+ if (fwrite(i, 1, verbatim, file) != verbatim) {
+ return -1;
+ }
+
+ i += verbatim;
+ switch (*i) {
+ case '%':
+ switch (*++i) {
+ case '%':
+ if (fputc('%', file) == EOF) {
+ return -1;
+ }
+ break;
+
+ case 'c':
+ if (fputc(va_arg(args, int), file) == EOF) {
+ return -1;
+ }
+ break;
+
+ case 'd':
+ if (fprintf(file, "%d", va_arg(args, int)) < 0) {
+ return -1;
+ }
+ break;
+
+ case 'g':
+ if (fprintf(file, "%g", va_arg(args, double)) < 0) {
+ return -1;
+ }
+ break;
+
+ case 's':
+ if (fputs(va_arg(args, const char *), file) == EOF) {
+ return -1;
+ }
+ break;
+
+ case 'z':
+ ++i;
+ if (*i != 'u') {
+ goto invalid;
+ }
+ if (fprintf(file, "%zu", va_arg(args, size_t)) < 0) {
+ return -1;
+ }
+ break;
+
+ case 'm':
+ if (fputs(strerror(error), file) == EOF) {
+ return -1;
+ }
+ break;
+
+ case 'p':
+ switch (*++i) {
+ 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;
+
+ default:
+ goto invalid;
+ }
+
+ break;
+
+ default:
+ goto invalid;
+ }
+ break;
+
+ case '$':
+ switch (*++i) {
+ case '$':
+ if (fputc('$', file) == EOF) {
+ 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(colors, *esc, file) != 0) {
+ return -1;
+ }
+ }
+
+ i = end;
+ break;
+ }
+
+ default:
+ goto invalid;
+ }
+ break;
+
+ default:
+ return 0;
+ }
+
+ }
+
+ return 0;
+
+invalid:
+ assert(false);
+ errno = EINVAL;
+ return -1;
+}
+/****************************************************************************
+ * 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. *
+ ****************************************************************************/
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+
+void bfs_error(const struct cmdline *cmdline, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_verror(cmdline, format, args);
+ va_end(args);
+}
+
+void bfs_warning(const struct cmdline *cmdline, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_vwarning(cmdline, format, args);
+ va_end(args);
+}
+
+void bfs_verror(const struct cmdline *cmdline, const char *format, va_list args) {
+ int error = errno;
+
+ bfs_error_prefix(cmdline);
+
+ errno = error;
+ cvfprintf(cmdline->cerr, format, args);
+}
+
+void bfs_vwarning(const struct cmdline *cmdline, const char *format, va_list args) {
+ int error = errno;
+
+ bfs_warning_prefix(cmdline);
+
+ errno = error;
+ cvfprintf(cmdline->cerr, format, args);
+}
+
+void bfs_error_prefix(const struct cmdline *cmdline) {
+ cfprintf(cmdline->cerr, "${bld}%s:${rs} ${er}error:${rs} ", xbasename(cmdline->argv[0]));
+}
+
+void bfs_warning_prefix(const struct cmdline *cmdline) {
+ cfprintf(cmdline->cerr, "${bld}%s:${rs} ${wr}warning:${rs} ", xbasename(cmdline->argv[0]));
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2016-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 <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 sizeof(struct dstring) + capacity + 1;
+}
+
+/** Allocate a dstring with the given contents. */
+static char *dstralloc_impl(size_t capacity, size_t length, const char *data) {
+ struct dstring *header = malloc(dstrsize(capacity));
+ if (!header) {
+ return NULL;
+ }
+
+ header->capacity = capacity;
+ header->length = length;
+ return memcpy(header->data, data, length + 1);
+}
+
+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);
+}
+
+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 dstrapp(char **str, char c) {
+ return dstrcat_impl(str, &c, 1);
+}
+
+char *dstrprintf(const char *format, ...) {
+ va_list args;
+
+ va_start(args, format);
+ int len = vsnprintf(NULL, 0, format, args);
+ va_end(args);
+
+ assert(len > 0);
+
+ char *str = dstralloc(len);
+ if (!str) {
+ return NULL;
+ }
+
+ va_start(args, format);
+ len = vsnprintf(str, len + 1, format, args);
+ va_end(args);
+
+ struct dstring *header = dstrheader(str);
+ assert(len == header->capacity);
+ header->length = len;
+
+ return str;
+}
+
+void dstrfree(char *dstr) {
+ if (dstr) {
+ free(dstrheader(dstr));
+ }
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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. *
+ ****************************************************************************/
+
+/**
+ * Implementation of all the literal expressions.
+ */
+
+#include <assert.h>
+#include <dirent.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>
+
+struct eval_state {
+ /** Data about the current file. */
+ const struct BFTW *ftwbuf;
+ /** The parsed command line. */
+ const struct cmdline *cmdline;
+ /** The bftw() callback return value. */
+ enum bftw_action action;
+ /** The eval_cmdline() return value. */
+ int *ret;
+ /** Whether to quit immediately. */
+ bool quit;
+};
+
+/**
+ * Print an error message.
+ */
+BFS_FORMATTER(2, 3)
+static void eval_error(struct eval_state *state, const char *format, ...) {
+ int error = errno;
+ const struct cmdline *cmdline = state->cmdline;
+ CFILE *cerr = cmdline->cerr;
+
+ bfs_error(cmdline, "%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 eval_state *state, int error) {
+ return state->cmdline->ignore_races
+ && is_nonexistence_error(error)
+ && state->ftwbuf->depth > 0;
+}
+
+/**
+ * Report an error that occurs during evaluation.
+ */
+static void eval_report_error(struct eval_state *state) {
+ if (!eval_should_ignore(state, errno)) {
+ eval_error(state, "%m.\n");
+ *state->ret = EXIT_FAILURE;
+ }
+}
+
+/**
+ * Perform a bfs_stat() call if necessary.
+ */
+static const struct bfs_stat *eval_stat(struct eval_state *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 expr_cmp(const struct expr *expr, long long n) {
+ switch (expr->cmp_flag) {
+ case CMP_EXACT:
+ return n == expr->idata;
+ case CMP_LESS:
+ return n < expr->idata;
+ case CMP_GREATER:
+ return n > expr->idata;
+ }
+
+ return false;
+}
+
+/**
+ * -true test.
+ */
+bool eval_true(const struct expr *expr, struct eval_state *state) {
+ return true;
+}
+
+/**
+ * -false test.
+ */
+bool eval_false(const struct expr *expr, struct eval_state *state) {
+ return false;
+}
+
+/**
+ * -executable, -readable, -writable tests.
+ */
+bool eval_access(const struct expr *expr, struct eval_state *state) {
+ const struct BFTW *ftwbuf = state->ftwbuf;
+ return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, expr->idata) == 0;
+}
+
+/**
+ * -acl test.
+ */
+bool eval_acl(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *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 eval_state *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));
+ *state->ret = EXIT_FAILURE;
+ }
+ return ret;
+}
+
+/**
+ * -[aBcm]?newer tests.
+ */
+bool eval_newer(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *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 MINUTES:
+ diff /= 60;
+ break;
+ case DAYS:
+ diff /= 60*60*24;
+ break;
+ }
+
+ return expr_cmp(expr, diff);
+}
+
+/**
+ * -used test.
+ */
+bool eval_used(const struct expr *expr, struct eval_state *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;
+ }
+
+ time_t diff = timespec_diff(atime, ctime);
+ diff /= 60*60*24;
+ return expr_cmp(expr, diff);
+}
+
+/**
+ * -gid test.
+ */
+bool eval_gid(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ return expr_cmp(expr, statbuf->gid);
+}
+
+/**
+ * -uid test.
+ */
+bool eval_uid(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ return expr_cmp(expr, statbuf->uid);
+}
+
+/**
+ * -nogroup test.
+ */
+bool eval_nogroup(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ errno = 0;
+ if (getgrgid(statbuf->gid) == NULL) {
+ if (errno == 0) {
+ return true;
+ } else {
+ eval_report_error(state);
+ }
+ }
+
+ return false;
+}
+
+/**
+ * -nouser test.
+ */
+bool eval_nouser(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ errno = 0;
+ if (getpwuid(statbuf->uid) == NULL) {
+ if (errno == 0) {
+ return true;
+ } else {
+ eval_report_error(state);
+ }
+ }
+
+ return false;
+}
+
+/**
+ * -delete action.
+ */
+bool eval_delete(const struct expr *expr, struct eval_state *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 bftw_typeflag type = bftw_typeflag(ftwbuf, BFS_STAT_NOFOLLOW);
+ if (type == BFTW_DIR) {
+ flag |= AT_REMOVEDIR;
+ } else if (type == BFTW_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 expr *expr, const struct cmdline *cmdline) {
+ int ret = 0;
+ if (expr->execbuf && bfs_exec_finish(expr->execbuf) != 0) {
+ if (errno != 0) {
+ bfs_error(cmdline, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
+ }
+ ret = -1;
+ }
+ if (expr->lhs && eval_exec_finish(expr->lhs, cmdline) != 0) {
+ ret = -1;
+ }
+ if (expr->rhs && eval_exec_finish(expr->rhs, cmdline) != 0) {
+ ret = -1;
+ }
+ return ret;
+}
+
+/**
+ * -exec[dir]/-ok[dir] actions.
+ */
+bool eval_exec(const struct expr *expr, struct eval_state *state) {
+ bool ret = bfs_exec(expr->execbuf, state->ftwbuf) == 0;
+ if (errno != 0) {
+ eval_error(state, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
+ *state->ret = EXIT_FAILURE;
+ }
+ return ret;
+}
+
+/**
+ * -exit action.
+ */
+bool eval_exit(const struct expr *expr, struct eval_state *state) {
+ state->action = BFTW_STOP;
+ *state->ret = expr->idata;
+ state->quit = true;
+ return true;
+}
+
+/**
+ * -depth N test.
+ */
+bool eval_depth(const struct expr *expr, struct eval_state *state) {
+ return expr_cmp(expr, state->ftwbuf->depth);
+}
+
+/**
+ * -empty test.
+ */
+bool eval_empty(const struct expr *expr, struct eval_state *state) {
+ bool ret = false;
+ const struct BFTW *ftwbuf = state->ftwbuf;
+
+ if (ftwbuf->typeflag == BFTW_DIR) {
+ int dfd = openat(ftwbuf->at_fd, ftwbuf->at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
+ if (dfd < 0) {
+ eval_report_error(state);
+ goto done;
+ }
+
+ DIR *dir = fdopendir(dfd);
+ if (!dir) {
+ eval_report_error(state);
+ close(dfd);
+ goto done;
+ }
+
+ struct dirent *de;
+ if (xreaddir(dir, &de) == 0) {
+ ret = !de;
+ } else {
+ eval_report_error(state);
+ }
+
+ closedir(dir);
+ } else {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (statbuf) {
+ ret = statbuf->size == 0;
+ }
+ }
+
+done:
+ return ret;
+}
+
+/**
+ * -fstype test.
+ */
+bool eval_fstype(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ const char *type = bfs_fstype(state->cmdline->mtab, statbuf);
+ return strcmp(type, expr->sdata) == 0;
+}
+
+/**
+ * -hidden test.
+ */
+bool eval_hidden(const struct expr *expr, struct eval_state *state) {
+ const struct BFTW *ftwbuf = state->ftwbuf;
+ return ftwbuf->nameoff > 0 && ftwbuf->path[ftwbuf->nameoff] == '.';
+}
+
+/**
+ * -nohidden action.
+ */
+bool eval_nohidden(const struct expr *expr, struct eval_state *state) {
+ if (eval_hidden(expr, state)) {
+ eval_prune(expr, state);
+ return false;
+ } else {
+ return true;
+ }
+}
+
+/**
+ * -inum test.
+ */
+bool eval_inum(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ return expr_cmp(expr, statbuf->ino);
+}
+
+/**
+ * -links test.
+ */
+bool eval_links(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ return expr_cmp(expr, statbuf->nlink);
+}
+
+/**
+ * -i?lname test.
+ */
+bool eval_lname(const struct expr *expr, struct eval_state *state) {
+ bool ret = false;
+ char *name = NULL;
+
+ const struct BFTW *ftwbuf = state->ftwbuf;
+ if (ftwbuf->typeflag != BFTW_LNK) {
+ goto done;
+ }
+
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ goto done;
+ }
+
+ name = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, statbuf->size);
+ if (!name) {
+ eval_report_error(state);
+ goto done;
+ }
+
+ ret = fnmatch(expr->sdata, name, expr->idata) == 0;
+
+done:
+ free(name);
+ return ret;
+}
+
+/**
+ * -i?name test.
+ */
+bool eval_name(const struct expr *expr, struct eval_state *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->sdata, name, expr->idata) == 0;
+ free(copy);
+ return ret;
+}
+
+/**
+ * -i?path test.
+ */
+bool eval_path(const struct expr *expr, struct eval_state *state) {
+ const struct BFTW *ftwbuf = state->ftwbuf;
+ return fnmatch(expr->sdata, ftwbuf->path, expr->idata) == 0;
+}
+
+/**
+ * -perm test.
+ */
+bool eval_perm(const struct expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ mode_t mode = statbuf->mode;
+ mode_t target;
+ if (state->ftwbuf->typeflag == BFTW_DIR) {
+ target = expr->dir_mode;
+ } else {
+ target = expr->file_mode;
+ }
+
+ switch (expr->mode_cmp) {
+ case MODE_EXACT:
+ return (mode & 07777) == target;
+
+ case MODE_ALL:
+ return (mode & target) == target;
+
+ case MODE_ANY:
+ return !(mode & target) == !target;
+ }
+
+ return false;
+}
+
+/**
+ * -f?ls action.
+ */
+bool eval_fls(const struct expr *expr, struct eval_state *state) {
+ CFILE *cfile = expr->cfile;
+ FILE *file = cfile->file;
+ 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 blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 1023)/1024;
+ char mode[11];
+ format_mode(statbuf->mode, mode);
+ uintmax_t nlink = statbuf->nlink;
+ if (fprintf(file, "%9ju %6ju %s %3ju ", ino, blocks, mode, nlink) < 0) {
+ goto error;
+ }
+
+ uintmax_t uid = statbuf->uid;
+ struct passwd *pwd = getpwuid(uid);
+ 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;
+ struct group *grp = getgrgid(gid);
+ if (grp) {
+ if (fprintf(file, " %-8s", grp->gr_name) < 0) {
+ goto error;
+ }
+ } else {
+ if (fprintf(file, " %-8ju", gid) < 0) {
+ goto error;
+ }
+ }
+
+ if (ftwbuf->typeflag & (BFTW_BLK | BFTW_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->typeflag == BFTW_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 expr *expr, struct eval_state *state) {
+ if (cfprintf(expr->cfile, "%pP\n", state->ftwbuf) < 0) {
+ eval_report_error(state);
+ }
+ return true;
+}
+
+/**
+ * -f?print0 action.
+ */
+bool eval_fprint0(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
+ if (bfs_printf(expr->cfile->file, expr->printf, state->ftwbuf) != 0) {
+ eval_report_error(state);
+ }
+
+ return true;
+}
+
+/**
+ * -printx action.
+ */
+bool eval_fprintx(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
+ state->action = BFTW_PRUNE;
+ return true;
+}
+
+/**
+ * -quit action.
+ */
+bool eval_quit(const struct expr *expr, struct eval_state *state) {
+ state->action = BFTW_STOP;
+ state->quit = true;
+ return true;
+}
+
+/**
+ * -i?regex test.
+ */
+bool eval_regex(const struct expr *expr, struct eval_state *state) {
+ const char *path = state->ftwbuf->path;
+ size_t len = strlen(path);
+ regmatch_t match = {
+ .rm_so = 0,
+ .rm_eo = len,
+ };
+
+ int flags = 0;
+#ifdef REG_STARTEND
+ flags |= REG_STARTEND;
+#endif
+ int err = regexec(expr->regex, path, 1, &match, flags);
+ if (err == 0) {
+ return match.rm_so == 0 && match.rm_eo == len;
+ } else if (err != REG_NOMATCH) {
+ char *str = xregerror(err, expr->regex);
+ if (str) {
+ eval_error(state, "%s.\n", str);
+ free(str);
+ } else {
+ perror("xregerror()");
+ }
+
+ *state->ret = EXIT_FAILURE;
+ }
+
+ return false;
+}
+
+/**
+ * -samefile test.
+ */
+bool eval_samefile(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
+ const struct bfs_stat *statbuf = eval_stat(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ static const off_t scales[] = {
+ [SIZE_BLOCKS] = 512,
+ [SIZE_BYTES] = 1,
+ [SIZE_WORDS] = 2,
+ [SIZE_KB] = 1024,
+ [SIZE_MB] = 1024LL*1024,
+ [SIZE_GB] = 1024LL*1024*1024,
+ [SIZE_TB] = 1024LL*1024*1024*1024,
+ [SIZE_PB] = 1024LL*1024*1024*1024*1024,
+ };
+
+ off_t scale = scales[expr->size_unit];
+ off_t size = (statbuf->size + scale - 1)/scale; // Round up
+ return expr_cmp(expr, size);
+}
+
+/**
+ * -sparse test.
+ */
+bool eval_sparse(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
+ return state->ftwbuf->typeflag & expr->idata;
+}
+
+/**
+ * -xattr test.
+ */
+bool eval_xattr(const struct expr *expr, struct eval_state *state) {
+ int ret = bfs_check_xattrs(state->ftwbuf);
+ if (ret >= 0) {
+ return ret;
+ } else {
+ eval_report_error(state);
+ return false;
+ }
+}
+
+/**
+ * -xtype test.
+ */
+bool eval_xtype(const struct expr *expr, struct eval_state *state) {
+ const struct BFTW *ftwbuf = state->ftwbuf;
+ enum bfs_stat_flag flags = ftwbuf->stat_flags ^ (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW);
+ enum bftw_typeflag type = bftw_typeflag(ftwbuf, flags);
+ if (type == BFTW_ERROR) {
+ eval_report_error(state);
+ return false;
+ } else {
+ return type & expr->idata;
+ }
+}
+
+#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 timespec *ts) {
+#ifdef BFS_CLOCK
+ int ret = clock_gettime(BFS_CLOCK, ts);
+ if (ret != 0) {
+ perror("clock_gettime()");
+ }
+ return ret;
+#else
+ return -1;
+#endif
+}
+
+/**
+ * Record the time that elapsed evaluating an expression.
+ */
+static void add_elapsed(struct expr *expr, const struct timespec *start, const struct timespec *end) {
+ expr->elapsed.tv_sec += end->tv_sec - start->tv_sec;
+ expr->elapsed.tv_nsec += end->tv_nsec - start->tv_nsec;
+ if (expr->elapsed.tv_nsec < 0) {
+ expr->elapsed.tv_nsec += 1000000000L;
+ --expr->elapsed.tv_sec;
+ } else if (expr->elapsed.tv_nsec >= 1000000000L) {
+ expr->elapsed.tv_nsec -= 1000000000L;
+ ++expr->elapsed.tv_sec;
+ }
+}
+
+/**
+ * Evaluate an expression.
+ */
+static bool eval_expr(struct expr *expr, struct eval_state *state) {
+ struct timespec start, end;
+ bool time = state->cmdline->debug & DEBUG_RATES;
+ if (time) {
+ if (eval_gettime(&start) != 0) {
+ time = false;
+ }
+ }
+
+ assert(!state->quit);
+
+ bool ret = expr->eval(expr, state);
+
+ if (time) {
+ if (eval_gettime(&end) == 0) {
+ add_elapsed(expr, &start, &end);
+ }
+ }
+
+ ++expr->evaluations;
+ if (ret) {
+ ++expr->successes;
+ }
+
+ if (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 expr *expr, struct eval_state *state) {
+ return !eval_expr(expr->rhs, state);
+}
+
+/**
+ * Evaluate a conjunction.
+ */
+bool eval_and(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
+ eval_expr(expr->lhs, state);
+
+ if (state->quit) {
+ return false;
+ }
+
+ return eval_expr(expr->rhs, state);
+}
+
+/** Check if we've seen a file before. */
+static bool eval_file_unique(struct eval_state *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 BFTW *ftwbuf, const struct bftw_stat *cache, enum bfs_stat_flag flags) {
+ 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 BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf;
+ if (statbuf || ftwbuf->stat_cache.error) {
+ debug_stat(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(ftwbuf, &ftwbuf->lstat_cache, BFS_STAT_NOFOLLOW);
+ }
+}
+
+/**
+ * Dump the bftw_typeflag for -D search.
+ */
+static const char *dump_bftw_typeflag(enum bftw_typeflag type) {
+#define DUMP_BFTW_TYPEFLAG_CASE(flag) \
+ case flag: \
+ return #flag
+
+ switch (type) {
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_BLK);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_CHR);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_DIR);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_DOOR);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_FIFO);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_LNK);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_PORT);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_REG);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_SOCK);
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_WHT);
+
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_ERROR);
+
+ default:
+ DUMP_BFTW_TYPEFLAG_CASE(BFTW_UNKNOWN);
+ }
+}
+
+#define DUMP_BFTW_MAP(value) [value] = #value
+
+/**
+ * Dump the bftw_visit for -D search.
+ */
+static const char *dump_bftw_visit(enum bftw_visit visit) {
+ static const char *visits[] = {
+ DUMP_BFTW_MAP(BFTW_PRE),
+ DUMP_BFTW_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_BFTW_MAP(BFTW_CONTINUE),
+ DUMP_BFTW_MAP(BFTW_PRUNE),
+ DUMP_BFTW_MAP(BFTW_STOP),
+ };
+ return actions[action];
+}
+
+/**
+ * Type passed as the argument to the bftw() callback.
+ */
+struct callback_args {
+ /** The parsed command line. */
+ const struct cmdline *cmdline;
+ /** The set of seen files. */
+ struct trie *seen;
+ /** Eventual return value from eval_cmdline(). */
+ int ret;
+};
+
+/**
+ * bftw() callback.
+ */
+static enum bftw_action cmdline_callback(const struct BFTW *ftwbuf, void *ptr) {
+ struct callback_args *args = ptr;
+
+ const struct cmdline *cmdline = args->cmdline;
+
+ struct eval_state state;
+ state.ftwbuf = ftwbuf;
+ state.cmdline = cmdline;
+ state.action = BFTW_CONTINUE;
+ state.ret = &args->ret;
+ state.quit = false;
+
+ if (ftwbuf->typeflag == BFTW_ERROR) {
+ if (!eval_should_ignore(&state, ftwbuf->error)) {
+ args->ret = EXIT_FAILURE;
+ eval_error(&state, "%s.\n", strerror(ftwbuf->error));
+ }
+ state.action = BFTW_PRUNE;
+ goto done;
+ }
+
+ if (cmdline->unique && ftwbuf->visit == BFTW_PRE) {
+ if (!eval_file_unique(&state, args->seen)) {
+ goto done;
+ }
+ }
+
+ if (cmdline->xargs_safe && strpbrk(ftwbuf->path, " \t\n\'\"\\")) {
+ args->ret = EXIT_FAILURE;
+ eval_error(&state, "Path is not safe for xargs.\n");
+ state.action = BFTW_PRUNE;
+ goto done;
+ }
+
+ if (cmdline->maxdepth < 0 || ftwbuf->depth >= cmdline->maxdepth) {
+ state.action = BFTW_PRUNE;
+ }
+
+ // In -depth mode, only handle directories on the BFTW_POST visit
+ enum bftw_visit expected_visit = BFTW_PRE;
+ if ((cmdline->flags & BFTW_DEPTH)
+ && (cmdline->strategy == BFTW_IDS || ftwbuf->typeflag == BFTW_DIR)
+ && ftwbuf->depth < cmdline->maxdepth) {
+ expected_visit = BFTW_POST;
+ }
+
+ if (ftwbuf->visit == expected_visit
+ && ftwbuf->depth >= cmdline->mindepth
+ && ftwbuf->depth <= cmdline->maxdepth) {
+ eval_expr(cmdline->expr, &state);
+ }
+
+done:
+ if (cmdline->debug & DEBUG_STAT) {
+ debug_stats(ftwbuf);
+ }
+
+ if (cmdline->debug & DEBUG_SEARCH) {
+ fprintf(stderr, "cmdline_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.typeflag = %s,\n", dump_bftw_typeflag(ftwbuf->typeflag));
+ fprintf(stderr, "\t.error = %d,\n", ftwbuf->error);
+ fprintf(stderr, "}) == %s\n", dump_bftw_action(state.action));
+ }
+
+ return state.action;
+}
+
+/**
+ * Infer the number of open file descriptors we're allowed to have.
+ */
+static int infer_fdlimit(const struct cmdline *cmdline) {
+ int ret = 4096;
+
+ struct rlimit rl;
+ if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
+ if (rl.rlim_cur != RLIM_INFINITY) {
+ ret = rl.rlim_cur;
+ }
+ }
+
+ // 3 for std{in,out,err}
+ int nopen = 3 + cmdline->nopen_files;
+
+ // Check /proc/self/fd for the current number of open fds, if possible
+ // (we may have inherited more than just the standard ones)
+ DIR *dir = opendir("/proc/self/fd");
+ if (!dir) {
+ dir = opendir("/dev/fd");
+ }
+ if (dir) {
+ // Account for 'dir' itself
+ nopen = -1;
+
+ struct dirent *de;
+ while (xreaddir(dir, &de) == 0 && de) {
+ ++nopen;
+ }
+
+ closedir(dir);
+ }
+
+ ret -= nopen;
+ ret -= cmdline->expr->persistent_fds;
+ ret -= cmdline->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_DEPTH);
+ DEBUG_FLAG(flags, BFTW_COMFOLLOW);
+ DEBUG_FLAG(flags, BFTW_LOGICAL);
+ DEBUG_FLAG(flags, BFTW_DETECT_CYCLES);
+ DEBUG_FLAG(flags, BFTW_XDEV);
+
+ assert(!flags);
+}
+
+/**
+ * Dump the bftw_strategy for -D search.
+ */
+static const char *dump_bftw_strategy(enum bftw_strategy strategy) {
+ static const char *strategies[] = {
+ DUMP_BFTW_MAP(BFTW_BFS),
+ DUMP_BFTW_MAP(BFTW_DFS),
+ DUMP_BFTW_MAP(BFTW_IDS),
+ };
+ return strategies[strategy];
+}
+
+/**
+ * Evaluate the command line.
+ */
+int eval_cmdline(const struct cmdline *cmdline) {
+ if (!cmdline->expr) {
+ return EXIT_SUCCESS;
+ }
+
+ struct callback_args args = {
+ .cmdline = cmdline,
+ .ret = EXIT_SUCCESS,
+ };
+
+ struct trie seen;
+ if (cmdline->unique) {
+ trie_init(&seen);
+ args.seen = &seen;
+ }
+
+ struct bftw_args bftw_args = {
+ .paths = cmdline->paths,
+ .npaths = cmdline->npaths,
+ .callback = cmdline_callback,
+ .ptr = &args,
+ .nopenfd = infer_fdlimit(cmdline),
+ .flags = cmdline->flags,
+ .strategy = cmdline->strategy,
+ .mtab = cmdline->mtab,
+ };
+
+ if (cmdline->debug & DEBUG_SEARCH) {
+ fprintf(stderr, "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 = cmdline_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, "cmdline->mtab");
+ } else {
+ fprintf(stderr, "NULL");
+ }
+ fprintf(stderr, ",\n})\n");
+ }
+
+ if (bftw(&bftw_args) != 0) {
+ args.ret = EXIT_FAILURE;
+ perror("bftw()");
+ }
+
+ if (eval_exec_finish(cmdline->expr, cmdline) != 0) {
+ args.ret = EXIT_FAILURE;
+ }
+
+ if (cmdline->debug & DEBUG_RATES) {
+ dump_cmdline(cmdline, true);
+ }
+
+ if (cmdline->unique) {
+ trie_destroy(&seen);
+ }
+
+ return args.ret;
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017-2018 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 <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/** Print some debugging info. */
+static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) {
+ if (!(execbuf->flags & BFS_EXEC_DEBUG)) {
+ return;
+ }
+
+ if (execbuf->flags & BFS_EXEC_CONFIRM) {
+ fputs("-ok", stderr);
+ } else {
+ fputs("-exec", stderr);
+ }
+ if (execbuf->flags & BFS_EXEC_CHDIR) {
+ fputs("dir", stderr);
+ }
+ fputs(": ", stderr);
+
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+}
+
+extern char **environ;
+
+/** 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*1024*1024)
+
+/** 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
+ 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;
+}
+
+struct bfs_exec *parse_bfs_exec(char **argv, enum bfs_exec_flags flags, const struct cmdline *cmdline) {
+ struct bfs_exec *execbuf = malloc(sizeof(*execbuf));
+ if (!execbuf) {
+ perror("malloc()");
+ goto fail;
+ }
+
+ execbuf->flags = flags;
+ execbuf->argv = NULL;
+ execbuf->argc = 0;
+ execbuf->argv_cap = 0;
+ execbuf->arg_size = 0;
+ execbuf->arg_max = 0;
+ execbuf->wd_fd = -1;
+ execbuf->wd_path = NULL;
+ execbuf->wd_len = 0;
+ execbuf->ret = 0;
+
+ if (cmdline->debug & DEBUG_EXEC) {
+ execbuf->flags |= BFS_EXEC_DEBUG;
+ }
+
+ size_t i;
+ for (i = 1; ; ++i) {
+ const char *arg = argv[i];
+ if (!arg) {
+ if (execbuf->flags & BFS_EXEC_CONFIRM) {
+ bfs_error(cmdline, "%s: Expected '... ;'.\n", argv[0]);
+ } else {
+ bfs_error(cmdline, "%s: Expected '... ;' or '... {} +'.\n", argv[0]);
+ }
+ goto fail;
+ } else if (strcmp(arg, ";") == 0) {
+ break;
+ } else if (strcmp(arg, "+") == 0) {
+ if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(argv[i - 1], "{}") == 0) {
+ execbuf->flags |= BFS_EXEC_MULTI;
+ break;
+ }
+ }
+ }
+
+ execbuf->tmpl_argv = argv + 1;
+ execbuf->tmpl_argc = i - 1;
+
+ if (execbuf->tmpl_argc == 0) {
+ bfs_error(cmdline, "%s: Missing command.\n", argv[0]);
+ goto fail;
+ }
+
+ execbuf->argv_cap = execbuf->tmpl_argc + 1;
+ execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv));
+ if (!execbuf->argv) {
+ perror("malloc()");
+ goto fail;
+ }
+
+ if (execbuf->flags & BFS_EXEC_MULTI) {
+ for (i = 0; i < execbuf->tmpl_argc - 1; ++i) {
+ char *arg = execbuf->tmpl_argv[i];
+ if (strstr(arg, "{}")) {
+ bfs_error(cmdline, "%s ... +: Only one '{}' is supported.\n", argv[0]);
+ goto fail;
+ }
+ execbuf->argv[i] = arg;
+ }
+ execbuf->argc = execbuf->tmpl_argc - 1;
+
+ execbuf->arg_max = bfs_exec_arg_max(execbuf);
+ }
+
+ return execbuf;
+
+fail:
+ free_bfs_exec(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 int bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) {
+ int ret = 0;
+
+ if (execbuf->wd_fd >= 0) {
+ if (!ftwbuf || execbuf->wd_fd != ftwbuf->at_fd) {
+ ret = close(execbuf->wd_fd);
+ }
+ execbuf->wd_fd = -1;
+ }
+
+ if (execbuf->wd_path) {
+ free(execbuf->wd_path);
+ execbuf->wd_path = NULL;
+ execbuf->wd_len = 0;
+ }
+
+ return ret;
+}
+
+/** 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) {
+ fprintf(stderr, "%s ", execbuf->argv[i]);
+ }
+ fprintf(stderr, "? ");
+
+ if (ynprompt() <= 0) {
+ errno = 0;
+ return -1;
+ }
+ }
+
+ 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;
+ }
+
+ 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, environ);
+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);
+ bfs_exec_debug(execbuf, "Command '%s' terminated by signal %d\n", execbuf->argv[0], sig);
+ } else {
+ bfs_exec_debug(execbuf, "Command '%s' 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;
+}
+
+/** 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 || error != E2BIG) {
+ break;
+ }
+
+ // Try to recover from E2BIG by trying fewer and fewer arguments
+ // until they fit
+ bfs_exec_debug(execbuf, "Got E2BIG, shrinking argument list...\n");
+ 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;
+ size_t new_size = execbuf->arg_size;
+
+ 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) {
+ execbuf->arg_max = new_size;
+ bfs_exec_debug(execbuf, "ARG_MAX: %zu\n", execbuf->arg_max);
+
+ // 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 next_size = execbuf->arg_size + bfs_exec_arg_size(arg);
+ if (next_size > execbuf->arg_max) {
+ bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n",
+ next_size, execbuf->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 free_bfs_exec(struct bfs_exec *execbuf) {
+ if (execbuf) {
+ bfs_exec_closewd(execbuf, NULL);
+ free(execbuf->argv);
+ free(execbuf);
+ }
+}
+/****************************************************************************
+ * 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. *
+ ****************************************************************************/
+
+#include <assert.h>
+#include <errno.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_CAN_CHECK_XATTRS
+# 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
+
+#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 any ACLs of the given type are non-trivial. */
+static int bfs_check_acl_type(const char *path, acl_type_t type) {
+ acl_t acl = acl_get_file(path, type);
+ if (!acl) {
+ if (is_absence_error(errno)) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+
+ 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)
+ acl_tag_t tag;
+ if (acl_get_tag_type(entry, &tag) != 0) {
+ continue;
+ }
+ if (tag != ACL_USER_OBJ && tag != ACL_GROUP_OBJ && tag != ACL_OTHER) {
+ ret = 1;
+ break;
+ }
+#else
+ ret = 1;
+ break;
+#endif
+ }
+
+ acl_free(acl);
+ return ret;
+}
+
+int bfs_check_acl(const struct BFTW *ftwbuf) {
+ if (ftwbuf->typeflag == BFTW_LNK) {
+ return 0;
+ }
+
+ const char *path = fake_at(ftwbuf);
+
+ int error = ENOTSUP;
+ int ret = -1;
+
+#if __APPLE__
+ // macOS gives EINVAL for either of the two standard ACL types,
+ // supporting only ACL_TYPE_EXTENDED
+ if (ret <= 0) {
+ ret = bfs_check_acl_type(path, ACL_TYPE_EXTENDED);
+ if (ret < 0) {
+ error = errno;
+ }
+ }
+#else
+ if (ret <= 0) {
+ ret = bfs_check_acl_type(path, ACL_TYPE_ACCESS);
+ if (ret < 0) {
+ error = errno;
+ }
+ }
+
+ if (ret <= 0 && ftwbuf->typeflag == BFTW_DIR) {
+ ret = bfs_check_acl_type(path, ACL_TYPE_DEFAULT);
+ if (ret < 0) {
+ error = errno;
+ }
+ }
+#endif
+
+ 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->typeflag == BFTW_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 __APPLE__
+ int options = ftwbuf->typeflag == BFTW_LNK ? XATTR_NOFOLLOW : 0;
+ len = listxattr(path, NULL, 0, options);
+#else
+ if (ftwbuf->typeflag == BFTW_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;
+ }
+}
+
+#else // !BFS_CAN_CHECK_XATTRS
+
+int bfs_check_xattrs(const struct BFTW *ftwbuf) {
+ errno = ENOTSUP;
+ return -1;
+}
+
+#endif
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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. *
+ ****************************************************************************/
+
+/**
+ * - main(): the entry point for bfs(1), a breadth-first version of find(1)
+ * - main.c (this file)
+ *
+ * - parse_cmdline(): parses the command line into an expression tree
+ * - cmdline.h (declares the parsed command line structure)
+ * - expr.h (declares the expression tree nodes)
+ * - parse.c (the parser itself)
+ * - opt.c (the expression optimizer)
+ *
+ * - eval_cmdline(): 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 eval_cmdline() to walk the directory tree(s)
+ * - bftw.[ch] (an extended version of nftw(3))
+ *
+ * - Utilities:
+ * - bfs.h (constants about bfs itself)
+ * - color.[ch] (for pretty terminal colors)
+ * - diag.[ch] (formats diagnostic messages)
+ * - dstring.[ch] (a dynamic string library)
+ * - fsade.[ch] (a facade over non-standard filesystem features)
+ * - mtab.[ch] (parses the system's mount table)
+ * - spawn.[ch] (spawns processes)
+ * - 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)
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/**
+ * 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 cmdline *cmdline = parse_cmdline(argc, argv);
+ if (cmdline) {
+ ret = eval_cmdline(cmdline);
+ }
+
+ if (free_cmdline(cmdline) != 0 && ret == EXIT_SUCCESS) {
+ ret = EXIT_FAILURE;
+ }
+
+done:
+ return ret;
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017-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 <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.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
+
+struct bfs_mtab {
+ /** A map from device ID to file system type. */
+ struct trie types;
+ /** The names of all the mount points. */
+ struct trie names;
+};
+
+/**
+ * Add an entry to the mount table.
+ */
+static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, dev_t dev, const char *type) {
+ if (!trie_insert_str(&mtab->names, xbasename(path))) {
+ return -1;
+ }
+
+ struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &dev, sizeof(dev));
+ if (!leaf) {
+ return -1;
+ }
+
+ if (leaf->value) {
+ return 0;
+ }
+
+ leaf->value = strdup(type);
+ if (leaf->value) {
+ return 0;
+ } else {
+ trie_remove(&mtab->types, leaf);
+ return -1;
+ }
+}
+
+struct bfs_mtab *parse_bfs_mtab() {
+#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
+ file = setmntent("/proc/mounts", "r");
+ }
+ if (!file) {
+ goto fail;
+ }
+
+ struct bfs_mtab *mtab = malloc(sizeof(*mtab));
+ if (!mtab) {
+ goto fail_file;
+ }
+ trie_init(&mtab->types);
+ trie_init(&mtab->names);
+
+ struct mntent *mnt;
+ while ((mnt = getmntent(file))) {
+ struct bfs_stat sb;
+ if (bfs_stat(AT_FDCWD, mnt->mnt_dir, BFS_STAT_NOFOLLOW, &sb) != 0) {
+ continue;
+ }
+
+ if (bfs_mtab_add(mtab, mnt->mnt_dir, sb.dev, mnt->mnt_type) != 0) {
+ goto fail_mtab;
+ }
+ }
+
+ endmntent(file);
+ return mtab;
+
+fail_mtab:
+ free_bfs_mtab(mtab);
+fail_file:
+ endmntent(file);
+fail:
+ return NULL;
+
+#elif BFS_MNTINFO
+
+ struct statfs *mntbuf;
+ int size = getmntinfo(&mntbuf, MNT_WAIT);
+ if (size < 0) {
+ return NULL;
+ }
+
+ struct bfs_mtab *mtab = malloc(sizeof(*mtab));
+ if (!mtab) {
+ goto fail;
+ }
+ trie_init(&mtab->types);
+ trie_init(&mtab->names);
+
+ for (struct statfs *mnt = mntbuf; mnt < mntbuf + size; ++mnt) {
+ struct bfs_stat sb;
+ if (bfs_stat(AT_FDCWD, mnt->f_mntonname, BFS_STAT_NOFOLLOW, &sb) != 0) {
+ continue;
+ }
+
+ if (bfs_mtab_add(mtab, mnt->f_mntonname, sb.dev, mnt->f_fstypename) != 0) {
+ goto fail_mtab;
+ }
+ }
+
+ return mtab;
+
+fail_mtab:
+ free_bfs_mtab(mtab);
+fail:
+ return NULL;
+
+#elif BFS_MNTTAB
+
+ FILE *file = fopen(MNTTAB, "r");
+ if (!file) {
+ goto fail;
+ }
+
+ struct bfs_mtab *mtab = malloc(sizeof(*mtab));
+ if (!mtab) {
+ goto fail_file;
+ }
+ trie_init(&mtab->types);
+ trie_init(&mtab->names);
+
+ struct mnttab mnt;
+ while (getmntent(file, &mnt) == 0) {
+ struct bfs_stat sb;
+ if (bfs_stat(AT_FDCWD, mnt.mnt_mountp, BFS_STAT_NOFOLLOW, &sb) != 0) {
+ continue;
+ }
+
+ if (bfs_mtab_add(mtab, mnt.mnt_mountp, sb.dev, mnt.mnt_fstype) != 0) {
+ goto fail_mtab;
+ }
+ }
+
+ fclose(file);
+ return mtab;
+
+fail_mtab:
+ free_bfs_mtab(mtab);
+fail_file:
+ fclose(file);
+fail:
+ return NULL;
+
+#else
+
+ errno = ENOTSUP;
+ return NULL;
+#endif
+}
+
+const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) {
+ 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_maybe_mount(const struct bfs_mtab *mtab, const char *path) {
+ const char *name = xbasename(path);
+ return trie_find_str(&mtab->names, name);
+}
+
+void free_bfs_mtab(struct bfs_mtab *mtab) {
+ if (mtab) {
+ trie_destroy(&mtab->names);
+
+ struct trie_leaf *leaf;
+ while ((leaf = trie_first_leaf(&mtab->types))) {
+ free(leaf->value);
+ trie_remove(&mtab->types, leaf);
+ }
+ trie_destroy(&mtab->types);
+
+ free(mtab);
+ }
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017-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. *
+ ****************************************************************************/
+
+/**
+ * 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 <assert.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.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_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. */
+ MAX_RANGE,
+};
+
+/**
+ * Data flow facts about an evaluation point.
+ */
+struct opt_facts {
+ /** The value ranges we track. */
+ struct range ranges[MAX_RANGE];
+
+ /** Bitmask of possible file types. */
+ enum bftw_typeflag types;
+ /** Bitmask of possible link target types. */
+ enum bftw_typeflag xtypes;
+};
+
+/** Initialize some data flow facts. */
+static void facts_init(struct opt_facts *facts) {
+ for (int i = 0; i < MAX_RANGE; ++i) {
+ struct range *range = facts->ranges + i;
+ range->min = 0; // All ranges we currently track are non-negative
+ range->max = LLONG_MAX;
+ }
+
+ 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 < MAX_RANGE; ++i) {
+ range_union(result->ranges + i, lhs->ranges + i, rhs->ranges + i);
+ }
+
+ result->types = lhs->types | rhs->types;
+ result->xtypes = lhs->xtypes | rhs->xtypes;
+}
+
+/** Determine whether a fact set is impossible. */
+static bool facts_impossible(const struct opt_facts *facts) {
+ for (int i = 0; i < MAX_RANGE; ++i) {
+ if (range_impossible(facts->ranges + i)) {
+ 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 < MAX_RANGE; ++i) {
+ set_range_impossible(facts->ranges + i);
+ }
+
+ facts->types = 0;
+ facts->xtypes = 0;
+}
+
+/**
+ * Optimizer state.
+ */
+struct opt_state {
+ /** The command line we're optimizing. */
+ const struct cmdline *cmdline;
+
+ /** 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. */
+static void debug_opt(const struct opt_state *state, const char *format, ...) {
+ if (!(state->cmdline->debug & DEBUG_OPT)) {
+ return;
+ }
+
+ CFILE *cerr = state->cmdline->cerr;
+
+ va_list args;
+ va_start(args, format);
+
+ for (const char *i = format; *i != '\0'; ++i) {
+ if (*i == '%') {
+ switch (*++i) {
+ case 'd':
+ fprintf(cerr->file, "%d", va_arg(args, int));
+ break;
+
+ case 'e':
+ dump_expr(cerr, va_arg(args, const struct expr *), false);
+ break;
+
+ case 'g':
+ cfprintf(cerr, "${ylw}%g${rs}", va_arg(args, double));
+ break;
+
+ default:
+ assert(false);
+ break;
+ }
+ } else {
+ fputc(*i, stderr);
+ }
+ }
+
+ va_end(args);
+}
+
+/** Extract a child expression, freeing the outer expression. */
+static struct expr *extract_child_expr(struct expr *expr, struct expr **child) {
+ struct expr *ret = *child;
+ *child = NULL;
+ free_expr(expr);
+ return ret;
+}
+
+/**
+ * Negate an expression.
+ */
+static struct expr *negate_expr(struct expr *rhs, char **argv) {
+ if (rhs->eval == eval_not) {
+ return extract_child_expr(rhs, &rhs->rhs);
+ }
+
+ struct expr *expr = new_expr(eval_not, 1, argv);
+ if (!expr) {
+ free_expr(rhs);
+ return NULL;
+ }
+
+ expr->rhs = rhs;
+ return expr;
+}
+
+static struct expr *optimize_not_expr(const struct opt_state *state, struct expr *expr);
+static struct expr *optimize_and_expr(const struct opt_state *state, struct expr *expr);
+static struct expr *optimize_or_expr(const struct opt_state *state, struct expr *expr);
+
+/**
+ * Apply De Morgan's laws.
+ */
+static struct expr *de_morgan(const struct opt_state *state, struct expr *expr, char **argv) {
+ debug_opt(state, "-O1: De Morgan's laws: %e ", expr);
+
+ struct expr *parent = negate_expr(expr, argv);
+ if (!parent) {
+ return NULL;
+ }
+
+ bool has_parent = true;
+ if (parent->eval != eval_not) {
+ expr = parent;
+ has_parent = false;
+ }
+
+ if (expr->eval == eval_and) {
+ expr->eval = eval_or;
+ expr->argv = &fake_or_arg;
+ } else {
+ assert(expr->eval == eval_or);
+ expr->eval = eval_and;
+ expr->argv = &fake_and_arg;
+ }
+
+ expr->lhs = negate_expr(expr->lhs, argv);
+ expr->rhs = negate_expr(expr->rhs, argv);
+ if (!expr->lhs || !expr->rhs) {
+ free_expr(parent);
+ return NULL;
+ }
+
+ debug_opt(state, "<==> %e\n", parent);
+
+ if (expr->lhs->eval == eval_not) {
+ expr->lhs = optimize_not_expr(state, expr->lhs);
+ }
+ if (expr->rhs->eval == eval_not) {
+ expr->rhs = optimize_not_expr(state, expr->rhs);
+ }
+ if (!expr->lhs || !expr->rhs) {
+ free_expr(parent);
+ return NULL;
+ }
+
+ if (expr->eval == eval_and) {
+ expr = optimize_and_expr(state, expr);
+ } else {
+ expr = optimize_or_expr(state, expr);
+ }
+ if (!expr) {
+ if (has_parent) {
+ parent->rhs = NULL;
+ free_expr(parent);
+ }
+ return NULL;
+ }
+
+ if (has_parent) {
+ parent = optimize_not_expr(state, parent);
+ }
+ return parent;
+}
+
+/** Optimize an expression recursively. */
+static struct expr *optimize_expr_recursive(struct opt_state *state, struct expr *expr);
+
+/**
+ * Optimize a negation.
+ */
+static struct expr *optimize_not_expr(const struct opt_state *state, struct expr *expr) {
+ assert(expr->eval == eval_not);
+
+ struct expr *rhs = expr->rhs;
+
+ int optlevel = state->cmdline->optlevel;
+ if (optlevel >= 1) {
+ if (rhs == &expr_true) {
+ debug_opt(state, "-O1: constant propagation: %e <==> %e\n", expr, &expr_false);
+ free_expr(expr);
+ return &expr_false;
+ } else if (rhs == &expr_false) {
+ debug_opt(state, "-O1: constant propagation: %e <==> %e\n", expr, &expr_true);
+ free_expr(expr);
+ return &expr_true;
+ } else if (rhs->eval == eval_not) {
+ debug_opt(state, "-O1: double negation: %e <==> %e\n", expr, rhs->rhs);
+ return extract_child_expr(expr, &rhs->rhs);
+ } else if (expr_never_returns(rhs)) {
+ debug_opt(state, "-O1: reachability: %e <==> %e\n", expr, rhs);
+ return extract_child_expr(expr, &expr->rhs);
+ } else if ((rhs->eval == eval_and || rhs->eval == eval_or)
+ && (rhs->lhs->eval == eval_not || rhs->rhs->eval == 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 expr *optimize_not_expr_recursive(struct opt_state *state, struct 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:
+ free_expr(expr);
+ return NULL;
+}
+
+/** Optimize a conjunction. */
+static struct expr *optimize_and_expr(const struct opt_state *state, struct expr *expr) {
+ assert(expr->eval == eval_and);
+
+ struct expr *lhs = expr->lhs;
+ struct expr *rhs = expr->rhs;
+
+ int optlevel = state->cmdline->optlevel;
+ if (optlevel >= 1) {
+ if (lhs == &expr_true) {
+ debug_opt(state, "-O1: conjunction elimination: %e <==> %e\n", expr, rhs);
+ return extract_child_expr(expr, &expr->rhs);
+ } else if (rhs == &expr_true) {
+ debug_opt(state, "-O1: conjunction elimination: %e <==> %e\n", expr, lhs);
+ return extract_child_expr(expr, &expr->lhs);
+ } else if (lhs->always_false) {
+ debug_opt(state, "-O1: short-circuit: %e <==> %e\n", expr, lhs);
+ return extract_child_expr(expr, &expr->lhs);
+ } else if (lhs->always_true && rhs == &expr_false) {
+ debug_opt(state, "-O1: strength reduction: %e <==> ", expr);
+ struct expr *ret = extract_child_expr(expr, &expr->lhs);
+ ret = negate_expr(ret, &fake_not_arg);
+ if (ret) {
+ debug_opt(state, "%e\n", ret);
+ }
+ return ret;
+ } else if (optlevel >= 2 && lhs->pure && rhs == &expr_false) {
+ debug_opt(state, "-O2: purity: %e <==> %e\n", expr, rhs);
+ return extract_child_expr(expr, &expr->rhs);
+ } else if (lhs->eval == eval_not && rhs->eval == 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 expr *optimize_and_expr_recursive(struct opt_state *state, struct 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:
+ free_expr(expr);
+ return NULL;
+}
+
+/** Optimize a disjunction. */
+static struct expr *optimize_or_expr(const struct opt_state *state, struct expr *expr) {
+ assert(expr->eval == eval_or);
+
+ struct expr *lhs = expr->lhs;
+ struct expr *rhs = expr->rhs;
+
+ int optlevel = state->cmdline->optlevel;
+ if (optlevel >= 1) {
+ if (lhs->always_true) {
+ debug_opt(state, "-O1: short-circuit: %e <==> %e\n", expr, lhs);
+ return extract_child_expr(expr, &expr->lhs);
+ } else if (lhs == &expr_false) {
+ debug_opt(state, "-O1: disjunctive syllogism: %e <==> %e\n", expr, rhs);
+ return extract_child_expr(expr, &expr->rhs);
+ } else if (rhs == &expr_false) {
+ debug_opt(state, "-O1: disjunctive syllogism: %e <==> %e\n", expr, lhs);
+ return extract_child_expr(expr, &expr->lhs);
+ } else if (lhs->always_false && rhs == &expr_true) {
+ debug_opt(state, "-O1: strength reduction: %e <==> ", expr);
+ struct expr *ret = extract_child_expr(expr, &expr->lhs);
+ ret = negate_expr(ret, &fake_not_arg);
+ if (ret) {
+ debug_opt(state, "%e\n", ret);
+ }
+ return ret;
+ } else if (optlevel >= 2 && lhs->pure && rhs == &expr_true) {
+ debug_opt(state, "-O2: purity: %e <==> %e\n", expr, rhs);
+ return extract_child_expr(expr, &expr->rhs);
+ } else if (lhs->eval == eval_not && rhs->eval == 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 expr *optimize_or_expr_recursive(struct opt_state *state, struct 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:
+ free_expr(expr);
+ return NULL;
+}
+
+/** Optimize an expression in an ignored-result context. */
+static struct expr *ignore_result(const struct opt_state *state, struct expr *expr) {
+ int optlevel = state->cmdline->optlevel;
+
+ if (optlevel >= 1) {
+ while (true) {
+ if (expr->eval == eval_not) {
+ debug_opt(state, "-O1: ignored result: %e --> %e\n", expr, expr->rhs);
+ expr = extract_child_expr(expr, &expr->rhs);
+ } else if (optlevel >= 2
+ && (expr->eval == eval_and || expr->eval == eval_or || expr->eval == eval_comma)
+ && expr->rhs->pure) {
+ debug_opt(state, "-O2: ignored result: %e --> %e\n", expr, expr->lhs);
+ expr = extract_child_expr(expr, &expr->lhs);
+ } else {
+ break;
+ }
+ }
+
+ if (optlevel >= 2 && expr->pure && expr != &expr_false) {
+ debug_opt(state, "-O2: ignored result: %e --> %e\n", expr, &expr_false);
+ free_expr(expr);
+ expr = &expr_false;
+ }
+ }
+
+ return expr;
+}
+
+/** Optimize a comma expression. */
+static struct expr *optimize_comma_expr(const struct opt_state *state, struct expr *expr) {
+ assert(expr->eval == eval_comma);
+
+ struct expr *lhs = expr->lhs;
+ struct expr *rhs = expr->rhs;
+
+ int optlevel = state->cmdline->optlevel;
+ if (optlevel >= 1) {
+ lhs = expr->lhs = ignore_result(state, lhs);
+
+ if (expr_never_returns(lhs)) {
+ debug_opt(state, "-O1: reachability: %e <==> %e\n", expr, lhs);
+ return extract_child_expr(expr, &expr->lhs);
+ } else if ((lhs->always_true && rhs == &expr_true)
+ || (lhs->always_false && rhs == &expr_false)) {
+ debug_opt(state, "-O1: redundancy elimination: %e <==> %e\n", expr, lhs);
+ return extract_child_expr(expr, &expr->lhs);
+ } else if (optlevel >= 2 && lhs->pure) {
+ debug_opt(state, "-O2: purity: %e <==> %e\n", expr, rhs);
+ return extract_child_expr(expr, &expr->rhs);
+ }
+ }
+
+ expr->pure = lhs->pure && rhs->pure;
+ expr->always_true = expr_never_returns(lhs) || rhs->always_true;
+ expr->always_false = 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 expr *optimize_comma_expr_recursive(struct opt_state *state, struct 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:
+ free_expr(expr);
+ return NULL;
+}
+
+/** Infer data flow facts about an icmp-style ([+-]N) expression */
+static void infer_icmp_facts(struct opt_state *state, const struct 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->idata;
+
+ switch (expr->cmp_flag) {
+ case CMP_EXACT:
+ constrain_min(range_when_true, value);
+ constrain_max(range_when_true, value);
+ range_remove(range_when_false, value);
+ break;
+
+ case CMP_LESS:
+ constrain_min(range_when_false, value);
+ constrain_max(range_when_true, value);
+ range_remove(range_when_true, value);
+ break;
+
+ case CMP_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 -samefile expression. */
+static void infer_samefile_facts(struct opt_state *state, const struct 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 expr *expr) {
+ state->facts_when_true.types &= expr->idata;
+ state->facts_when_false.types &= ~expr->idata;
+}
+
+/** Infer data flow facts about an -xtype expression. */
+static void infer_xtype_facts(struct opt_state *state, const struct expr *expr) {
+ state->facts_when_true.xtypes &= expr->idata;
+ state->facts_when_false.xtypes &= ~expr->idata;
+}
+
+static struct expr *optimize_expr_recursive(struct opt_state *state, struct expr *expr) {
+ state->facts_when_true = state->facts;
+ state->facts_when_false = state->facts;
+
+ if (expr->eval == eval_depth) {
+ infer_icmp_facts(state, expr, DEPTH_RANGE);
+ } else if (expr->eval == eval_gid) {
+ infer_icmp_facts(state, expr, GID_RANGE);
+ } else if (expr->eval == eval_inum) {
+ infer_icmp_facts(state, expr, INUM_RANGE);
+ } else if (expr->eval == eval_links) {
+ infer_icmp_facts(state, expr, LINKS_RANGE);
+ } else if (expr->eval == eval_samefile) {
+ infer_samefile_facts(state, expr);
+ } else if (expr->eval == eval_size) {
+ infer_icmp_facts(state, expr, SIZE_RANGE);
+ } else if (expr->eval == eval_type) {
+ infer_type_facts(state, expr);
+ } else if (expr->eval == eval_uid) {
+ infer_icmp_facts(state, expr, UID_RANGE);
+ } else if (expr->eval == eval_xtype) {
+ infer_xtype_facts(state, expr);
+ } else if (expr->eval == eval_not) {
+ expr = optimize_not_expr_recursive(state, expr);
+ } else if (expr->eval == eval_and) {
+ expr = optimize_and_expr_recursive(state, expr);
+ } else if (expr->eval == eval_or) {
+ expr = optimize_or_expr_recursive(state, expr);
+ } else if (expr->eval == eval_comma) {
+ expr = optimize_comma_expr_recursive(state, expr);
+ } else if (!expr->pure) {
+ facts_union(state->facts_when_impure, state->facts_when_impure, &state->facts);
+ }
+
+ if (!expr) {
+ goto done;
+ }
+
+ struct expr *lhs = expr->lhs;
+ struct 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 (state->cmdline->optlevel < 2 || expr == &expr_true || expr == &expr_false) {
+ goto done;
+ }
+
+ if (facts_impossible(&state->facts_when_true)) {
+ if (expr->pure) {
+ debug_opt(state, "-O2: data flow: %e --> %e\n", expr, &expr_false);
+ free_expr(expr);
+ expr = &expr_false;
+ } else {
+ expr->always_false = true;
+ expr->probability = 0.0;
+ }
+ } else if (facts_impossible(&state->facts_when_false)) {
+ if (expr->pure) {
+ debug_opt(state, "-O2: data flow: %e --> %e\n", expr, &expr_true);
+ free_expr(expr);
+ expr = &expr_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 expr *expr, double swapped_cost) {
+ if (swapped_cost < expr->cost) {
+ debug_opt(state, "-O3: cost: %e", expr);
+ struct expr *lhs = expr->lhs;
+ expr->lhs = expr->rhs;
+ expr->rhs = lhs;
+ debug_opt(state, " <==> %e (~%g --> ~%g)\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 expr *expr) {
+ bool ret = false;
+ struct expr *lhs = expr->lhs;
+ struct expr *rhs = expr->rhs;
+
+ if (lhs) {
+ ret |= reorder_expr_recursive(state, lhs);
+ }
+ if (rhs) {
+ ret |= reorder_expr_recursive(state, rhs);
+ }
+
+ if (expr->eval == eval_and || expr->eval == eval_or) {
+ if (lhs->pure && rhs->pure) {
+ double rhs_prob = expr->eval == eval_and ? rhs->probability : 1.0 - rhs->probability;
+ double swapped_cost = rhs->cost + rhs_prob*lhs->cost;
+ ret |= reorder_expr(state, expr, swapped_cost);
+ }
+ }
+
+ return ret;
+}
+
+int optimize_cmdline(struct cmdline *cmdline) {
+ struct opt_facts facts_when_impure;
+ set_facts_impossible(&facts_when_impure);
+
+ struct opt_state state = {
+ .cmdline = cmdline,
+ .facts_when_impure = &facts_when_impure,
+ };
+ facts_init(&state.facts);
+
+ struct range *depth = state.facts.ranges + DEPTH_RANGE;
+ depth->min = cmdline->mindepth;
+ depth->max = cmdline->maxdepth;
+
+ int optlevel = cmdline->optlevel;
+
+ cmdline->expr = optimize_expr_recursive(&state, cmdline->expr);
+ if (!cmdline->expr) {
+ return -1;
+ }
+
+ if (optlevel >= 3 && reorder_expr_recursive(&state, cmdline->expr)) {
+ // Re-do optimizations to account for the new ordering
+ set_facts_impossible(&facts_when_impure);
+ cmdline->expr = optimize_expr_recursive(&state, cmdline->expr);
+ if (!cmdline->expr) {
+ return -1;
+ }
+ }
+
+ cmdline->expr = ignore_result(&state, cmdline->expr);
+
+ 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;
+
+ if (optlevel >= 2 && mindepth > cmdline->mindepth) {
+ if (mindepth > INT_MAX) {
+ mindepth = INT_MAX;
+ }
+ cmdline->mindepth = mindepth;
+ debug_opt(&state, "-O2: data flow: mindepth --> %d\n", cmdline->mindepth);
+ }
+
+ if (optlevel >= 4 && maxdepth < cmdline->maxdepth) {
+ if (maxdepth < INT_MIN) {
+ maxdepth = INT_MIN;
+ }
+ cmdline->maxdepth = maxdepth;
+ debug_opt(&state, "-O4: data flow: maxdepth --> %d\n", cmdline->maxdepth);
+ }
+
+ return 0;
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2015-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. *
+ ****************************************************************************/
+
+/**
+ * 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 <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <grp.h>
+#include <limits.h>
+#include <pwd.h>
+#include <regex.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/types.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_false_arg = "-false";
+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 expr expr_true = {
+ .eval = eval_true,
+ .lhs = NULL,
+ .rhs = NULL,
+ .pure = true,
+ .always_true = true,
+ .cost = FAST_COST,
+ .probability = 1.0,
+ .argc = 1,
+ .argv = &fake_true_arg,
+};
+
+struct expr expr_false = {
+ .eval = eval_false,
+ .lhs = NULL,
+ .rhs = NULL,
+ .pure = true,
+ .always_false = true,
+ .cost = FAST_COST,
+ .probability = 0.0,
+ .argc = 1,
+ .argv = &fake_false_arg,
+};
+
+/**
+ * Free an expression.
+ */
+void free_expr(struct expr *expr) {
+ if (!expr || expr == &expr_true || expr == &expr_false) {
+ return;
+ }
+
+ if (expr->regex) {
+ regfree(expr->regex);
+ free(expr->regex);
+ }
+
+ free_bfs_printf(expr->printf);
+ free_bfs_exec(expr->execbuf);
+
+ free_expr(expr->lhs);
+ free_expr(expr->rhs);
+
+ free(expr);
+}
+
+struct expr *new_expr(eval_fn *eval, size_t argc, char **argv) {
+ struct expr *expr = malloc(sizeof(*expr));
+ if (!expr) {
+ perror("malloc()");
+ return NULL;
+ }
+
+ expr->eval = eval;
+ expr->lhs = NULL;
+ expr->rhs = NULL;
+ expr->pure = false;
+ expr->always_true = false;
+ expr->always_false = 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;
+ expr->argc = argc;
+ expr->argv = argv;
+ expr->cfile = NULL;
+ expr->regex = NULL;
+ expr->execbuf = NULL;
+ expr->printf = NULL;
+ expr->persistent_fds = 0;
+ expr->ephemeral_fds = 0;
+ return expr;
+}
+
+/**
+ * Create a new unary expression.
+ */
+static struct expr *new_unary_expr(eval_fn *eval, struct expr *rhs, char **argv) {
+ struct expr *expr = new_expr(eval, 1, argv);
+ if (!expr) {
+ free_expr(rhs);
+ return NULL;
+ }
+
+ expr->rhs = rhs;
+ expr->persistent_fds = rhs->persistent_fds;
+ expr->ephemeral_fds = rhs->ephemeral_fds;
+ return expr;
+}
+
+/**
+ * Create a new binary expression.
+ */
+static struct expr *new_binary_expr(eval_fn *eval, struct expr *lhs, struct expr *rhs, char **argv) {
+ struct expr *expr = new_expr(eval, 1, argv);
+ if (!expr) {
+ free_expr(rhs);
+ free_expr(lhs);
+ return NULL;
+ }
+
+ expr->lhs = lhs;
+ expr->rhs = rhs;
+ 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;
+}
+
+/**
+ * Check if an expression never returns.
+ */
+bool expr_never_returns(const struct 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 expr *expr) {
+ expr->always_true = true;
+ expr->probability = 1.0;
+}
+
+/**
+ * Set an expression to never return.
+ */
+static void expr_set_never_returns(struct expr *expr) {
+ expr->always_true = expr->always_false = true;
+}
+
+/**
+ * Dump the parsed expression tree, for debugging.
+ */
+void dump_expr(CFILE *cfile, const struct expr *expr, bool verbose) {
+ fputs("(", cfile->file);
+
+ if (expr->lhs || expr->rhs) {
+ cfprintf(cfile, "${red}%s${rs}", expr->argv[0]);
+ } else {
+ cfprintf(cfile, "${blu}%s${rs}", expr->argv[0]);
+ }
+
+ for (size_t i = 1; i < expr->argc; ++i) {
+ cfprintf(cfile, " ${bld}%s${rs}", expr->argv[i]);
+ }
+
+ 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;
+ }
+ cfprintf(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]", expr->successes, expr->evaluations, rate, time);
+ }
+
+ if (expr->lhs) {
+ fputs(" ", cfile->file);
+ dump_expr(cfile, expr->lhs, verbose);
+ }
+
+ if (expr->rhs) {
+ fputs(" ", cfile->file);
+ dump_expr(cfile, expr->rhs, verbose);
+ }
+
+ fputs(")", cfile->file);
+}
+
+/**
+ * An open file for the command line.
+ */
+struct open_file {
+ /** The file itself. */
+ CFILE *cfile;
+ /** The path to the file (for diagnostics). */
+ const char *path;
+};
+
+/**
+ * Free the parsed command line.
+ */
+int free_cmdline(struct cmdline *cmdline) {
+ int ret = 0;
+
+ if (cmdline) {
+ CFILE *cout = cmdline->cout;
+ CFILE *cerr = cmdline->cerr;
+
+ free_expr(cmdline->expr);
+
+ free_bfs_mtab(cmdline->mtab);
+
+ struct trie_leaf *leaf;
+ while ((leaf = trie_first_leaf(&cmdline->open_files))) {
+ struct open_file *ofile = leaf->value;
+
+ if (cfclose(ofile->cfile) != 0) {
+ if (cerr) {
+ bfs_error(cmdline, "'%s': %m.\n", ofile->path);
+ }
+ ret = -1;
+ }
+
+ free(ofile);
+ trie_remove(&cmdline->open_files, leaf);
+ }
+ trie_destroy(&cmdline->open_files);
+
+ if (cout && fflush(cout->file) != 0) {
+ if (cerr) {
+ bfs_error(cmdline, "standard output: %m.\n");
+ }
+ ret = -1;
+ }
+
+ cfclose(cout);
+ cfclose(cerr);
+
+ free_colors(cmdline->colors);
+ free(cmdline->paths);
+ free(cmdline->argv);
+ free(cmdline);
+ }
+
+ return ret;
+}
+
+/**
+ * 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 cmdline *cmdline;
+ /** The command line arguments being parsed. */
+ char **argv;
+ /** The name of this program. */
+ const char *command;
+
+ /** The current regex flags to use. */
+ int regex_flags;
+
+ /** 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 warnings are enabled (see -warn, -nowarn). */
+ bool warn;
+ /** Whether the expression has started. */
+ bool expr_started;
+ /** Whether any non-option arguments have been encountered. */
+ bool non_option_seen;
+ /** Whether an information option like -help or -version was passed. */
+ bool just_info;
+
+ /** The last non-path argument. */
+ const char *last_arg;
+ /** A "-depth"-type argument if any. */
+ const char *depth_arg;
+ /** A "-prune"-type argument if any. */
+ const char *prune_arg;
+
+ /** 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 an error message during parsing.
+ */
+BFS_FORMATTER(2, 3)
+static void parse_error(const struct parser_state *state, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_verror(state->cmdline, format, args);
+ va_end(args);
+}
+
+/**
+ * Print a warning message during parsing.
+ */
+BFS_FORMATTER(2, 3)
+static void parse_warning(const struct parser_state *state, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_vwarning(state->cmdline, format, args);
+ va_end(args);
+}
+
+/**
+ * Fill in a "-print"-type expression.
+ */
+static void init_print_expr(struct parser_state *state, struct expr *expr) {
+ expr_set_always_true(expr);
+ expr->cost = PRINT_COST;
+ expr->cfile = state->cmdline->cout;
+}
+
+/**
+ * Open a file for an expression.
+ */
+static int expr_open(struct parser_state *state, struct expr *expr, const char *path) {
+ int ret = -1;
+
+ struct cmdline *cmdline = state->cmdline;
+
+ CFILE *cfile = cfopen(path, state->use_color ? cmdline->colors : NULL);
+ if (!cfile) {
+ parse_error(state, "'%s': %m.\n", path);
+ goto out;
+ }
+
+ struct bfs_stat sb;
+ if (bfs_stat(fileno(cfile->file), NULL, 0, &sb) != 0) {
+ parse_error(state, "'%s': %m.\n", path);
+ goto out_close;
+ }
+
+ bfs_file_id id;
+ bfs_stat_id(&sb, &id);
+
+ struct trie_leaf *leaf = trie_insert_mem(&cmdline->open_files, id, sizeof(id));
+ if (leaf->value) {
+ struct open_file *ofile = leaf->value;
+ expr->cfile = ofile->cfile;
+ ret = 0;
+ goto out_close;
+ }
+
+ struct open_file *ofile = malloc(sizeof(*ofile));
+ if (!ofile) {
+ perror("malloc()");
+ trie_remove(&cmdline->open_files, leaf);
+ goto out_close;
+ }
+
+ ofile->cfile = cfile;
+ ofile->path = path;
+ leaf->value = ofile;
+ ++cmdline->nopen_files;
+
+ expr->cfile = cfile;
+
+ ret = 0;
+ goto out;
+
+out_close:
+ cfclose(cfile);
+out:
+ return ret;
+}
+
+/**
+ * Invoke bfs_stat() on an argument.
+ */
+static int stat_arg(const struct parser_state *state, struct expr *expr, struct bfs_stat *sb) {
+ const struct cmdline *cmdline = state->cmdline;
+
+ bool follow = cmdline->flags & (BFTW_COMFOLLOW | BFTW_LOGICAL);
+ enum bfs_stat_flag flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW;
+
+ int ret = bfs_stat(AT_FDCWD, expr->sdata, flags, sb);
+ if (ret != 0) {
+ parse_error(state, "'%s': %m.\n", expr->sdata);
+ }
+ return ret;
+}
+
+/**
+ * Parse the expression specified on the command line.
+ */
+static struct 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_OPTION) {
+ state->non_option_seen = 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) {
+ struct cmdline *cmdline = state->cmdline;
+ size_t i = cmdline->npaths;
+ if ((i & (i + 1)) == 0) {
+ const char **paths = realloc(cmdline->paths, 2*(i + 1)*sizeof(*paths));
+ if (!paths) {
+ return -1;
+ }
+ cmdline->paths = paths;
+ }
+
+ cmdline->paths[i] = path;
+ cmdline->npaths = i + 1;
+ return 0;
+}
+
+/**
+ * While parsing an expression, skip any paths and add them to the cmdline.
+ */
+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, 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) {
+ goto bad;
+ }
+
+ if (endptr == str) {
+ goto bad;
+ }
+
+ if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') {
+ goto bad;
+ }
+
+ if ((flags & IF_UNSIGNED) && value < 0) {
+ goto bad;
+ }
+
+ switch (flags & IF_SIZE_MASK) {
+ case IF_INT:
+ if (value < INT_MIN || value > INT_MAX) {
+ goto bad;
+ }
+ *(int *)result = value;
+ break;
+
+ case IF_LONG:
+ if (value < LONG_MIN || value > LONG_MAX) {
+ goto bad;
+ }
+ *(long *)result = value;
+ break;
+
+ case IF_LONG_LONG:
+ *(long long *)result = value;
+ break;
+ }
+
+ return endptr;
+
+bad:
+ if (!(flags & IF_QUIET)) {
+ parse_error(state, "'%s' is not a valid integer.\n", str);
+ }
+ return NULL;
+}
+
+/**
+ * Parse an integer and a comparison flag.
+ */
+static const char *parse_icmp(const struct parser_state *state, const char *str, struct expr *expr, enum int_flags flags) {
+ switch (str[0]) {
+ case '-':
+ expr->cmp_flag = CMP_LESS;
+ ++str;
+ break;
+ case '+':
+ expr->cmp_flag = CMP_GREATER;
+ ++str;
+ break;
+ default:
+ expr->cmp_flag = CMP_EXACT;
+ break;
+ }
+
+ return parse_int(state, str, &expr->idata, 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 expr *parse_flag(struct parser_state *state, size_t argc) {
+ parser_advance(state, T_FLAG, argc);
+ return &expr_true;
+}
+
+/**
+ * Parse a flag that doesn't take a value.
+ */
+static struct expr *parse_nullary_flag(struct parser_state *state) {
+ return parse_flag(state, 1);
+}
+
+/**
+ * Parse a flag that takes a single value.
+ */
+static struct expr *parse_unary_flag(struct parser_state *state) {
+ return parse_flag(state, 2);
+}
+
+/**
+ * Parse a single option.
+ */
+static struct expr *parse_option(struct parser_state *state, size_t argc) {
+ const char *arg = *parser_advance(state, T_OPTION, argc);
+
+ if (state->warn && state->non_option_seen) {
+ parse_warning(state,
+ "The '%s' option applies to the entire command line. For clarity, place\n"
+ "it before any non-option arguments.\n\n",
+ arg);
+ }
+
+
+ return &expr_true;
+}
+
+/**
+ * Parse an option that doesn't take a value.
+ */
+static struct expr *parse_nullary_option(struct parser_state *state) {
+ return parse_option(state, 1);
+}
+
+/**
+ * Parse an option that takes a value.
+ */
+static struct expr *parse_unary_option(struct parser_state *state) {
+ return parse_option(state, 2);
+}
+
+/**
+ * Parse a single positional option.
+ */
+static struct expr *parse_positional_option(struct parser_state *state, size_t argc) {
+ parser_advance(state, T_OPTION, argc);
+ return &expr_true;
+}
+
+/**
+ * Parse a positional option that doesn't take a value.
+ */
+static struct expr *parse_nullary_positional_option(struct parser_state *state) {
+ return parse_positional_option(state, 1);
+}
+
+/**
+ * Parse a positional option that takes a single value.
+ */
+static struct expr *parse_unary_positional_option(struct parser_state *state, const char **value) {
+ const char *arg = state->argv[0];
+ *value = state->argv[1];
+ if (!*value) {
+ parse_error(state, "%s needs a value.\n", arg);
+ return NULL;
+ }
+
+ return parse_positional_option(state, 2);
+}
+
+/**
+ * Parse a single test.
+ */
+static struct expr *parse_test(struct parser_state *state, eval_fn *eval, size_t argc) {
+ char **argv = parser_advance(state, T_TEST, argc);
+ struct expr *expr = new_expr(eval, argc, argv);
+ if (expr) {
+ expr->pure = true;
+ }
+ return expr;
+}
+
+/**
+ * Parse a test that doesn't take a value.
+ */
+static struct expr *parse_nullary_test(struct parser_state *state, eval_fn *eval) {
+ return parse_test(state, eval, 1);
+}
+
+/**
+ * Parse a test that takes a value.
+ */
+static struct expr *parse_unary_test(struct parser_state *state, eval_fn *eval) {
+ const char *arg = state->argv[0];
+ const char *value = state->argv[1];
+ if (!value) {
+ parse_error(state, "%s needs a value.\n", arg);
+ return NULL;
+ }
+
+ struct expr *expr = parse_test(state, eval, 2);
+ if (expr) {
+ expr->sdata = value;
+ }
+ return expr;
+}
+
+/**
+ * Parse a single action.
+ */
+static struct expr *parse_action(struct parser_state *state, eval_fn *eval, size_t argc) {
+ if (eval != eval_nohidden && eval != eval_prune && eval != eval_quit) {
+ state->implicit_print = false;
+ }
+
+ char **argv = parser_advance(state, T_ACTION, argc);
+ return new_expr(eval, argc, argv);
+}
+
+/**
+ * Parse an action that takes no arguments.
+ */
+static struct expr *parse_nullary_action(struct parser_state *state, eval_fn *eval) {
+ return parse_action(state, eval, 1);
+}
+
+/**
+ * Parse an action that takes one argument.
+ */
+static struct expr *parse_unary_action(struct parser_state *state, eval_fn *eval) {
+ const char *arg = state->argv[0];
+ const char *value = state->argv[1];
+ if (!value) {
+ parse_error(state, "%s needs a value.\n", arg);
+ return NULL;
+ }
+
+ struct expr *expr = parse_action(state, eval, 2);
+ if (expr) {
+ expr->sdata = value;
+ }
+ return expr;
+}
+
+/**
+ * Parse a test expression with integer data and a comparison flag.
+ */
+static struct expr *parse_test_icmp(struct parser_state *state, eval_fn *eval) {
+ struct expr *expr = parse_unary_test(state, eval);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (!parse_icmp(state, expr->sdata, expr, 0)) {
+ free_expr(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");
+}
+
+/** A named debug flag. */
+struct debug_flag {
+ enum debug_flags flag;
+ const char *name;
+};
+
+/** The table of debug flags. */
+struct debug_flag debug_flags[] = {
+ {DEBUG_ALL, "all"},
+ {DEBUG_COST, "cost"},
+ {DEBUG_EXEC, "exec"},
+ {DEBUG_OPT, "opt"},
+ {DEBUG_RATES, "rates"},
+ {DEBUG_SEARCH, "search"},
+ {DEBUG_STAT, "stat"},
+ {DEBUG_TREE, "tree"},
+ {0},
+};
+
+/** 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 expr *parse_debug(struct parser_state *state, int arg1, int arg2) {
+ struct cmdline *cmdline = state->cmdline;
+
+ const char *arg = state->argv[0];
+ const char *flags = state->argv[1];
+ if (!flags) {
+ parse_error(state, "%s needs a flag.\n\n", arg);
+ debug_help(cmdline->cerr);
+ return NULL;
+ }
+
+ 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(cmdline->cout);
+ state->just_info = true;
+ return NULL;
+ }
+
+ for (int i = 0; ; ++i) {
+ const char *expected = debug_flags[i].name;
+ if (!expected) {
+ parse_warning(state, "Unrecognized debug flag '");
+ fwrite(flag, 1, len, stderr);
+ fputs("'\n\n", stderr);
+ unrecognized = true;
+ break;
+ }
+
+ if (parse_debug_flag(flag, len, expected)) {
+ cmdline->debug |= debug_flags[i].flag;
+ break;
+ }
+ }
+ }
+
+ if (unrecognized) {
+ debug_help(cmdline->cerr);
+ cfprintf(cmdline->cerr, "\n");
+ }
+
+ return parse_unary_flag(state);
+}
+
+/**
+ * Parse -On.
+ */
+static struct expr *parse_optlevel(struct parser_state *state, int arg1, int arg2) {
+ int *optlevel = &state->cmdline->optlevel;
+
+ if (strcmp(state->argv[0], "-Ofast") == 0) {
+ *optlevel = 4;
+ } else if (!parse_int(state, state->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) {
+ return NULL;
+ }
+
+ if (state->warn && *optlevel > 4) {
+ parse_warning(state, "%s is the same as -O4.\n\n", state->argv[0]);
+ }
+
+ return parse_nullary_flag(state);
+}
+
+/**
+ * Parse -[PHL], -(no)?follow.
+ */
+static struct expr *parse_follow(struct parser_state *state, int flags, int option) {
+ struct cmdline *cmdline = state->cmdline;
+ cmdline->flags &= ~(BFTW_COMFOLLOW | BFTW_LOGICAL);
+ cmdline->flags |= flags;
+ if (option) {
+ return parse_nullary_positional_option(state);
+ } else {
+ return parse_nullary_flag(state);
+ }
+}
+
+/**
+ * Parse -X.
+ */
+static struct expr *parse_xargs_safe(struct parser_state *state, int arg1, int arg2) {
+ state->cmdline->xargs_safe = true;
+ return parse_nullary_flag(state);
+}
+
+/**
+ * Parse -executable, -readable, -writable
+ */
+static struct expr *parse_access(struct parser_state *state, int flag, int arg2) {
+ struct expr *expr = parse_nullary_test(state, eval_access);
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->idata = 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 expr *parse_acl(struct parser_state *state, int flag, int arg2) {
+#if BFS_CAN_CHECK_ACL
+ struct expr *expr = parse_nullary_test(state, eval_acl);
+ if (expr) {
+ expr->cost = STAT_COST;
+ expr->probability = 0.00002;
+ }
+ return expr;
+#else
+ parse_error(state, "%s is missing platform support.\n", state->argv[0]);
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -[aBcm]?newer.
+ */
+static struct expr *parse_newer(struct parser_state *state, int field, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_newer);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_stat sb;
+ if (stat_arg(state, expr, &sb) != 0) {
+ goto fail;
+ }
+
+ expr->cost = STAT_COST;
+ expr->reftime = sb.mtime;
+ expr->stat_field = field;
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -[aBcm]{min,time}.
+ */
+static struct expr *parse_time(struct parser_state *state, int field, int unit) {
+ struct 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 = unit;
+ return expr;
+}
+
+/**
+ * Parse -capable.
+ */
+static struct expr *parse_capable(struct parser_state *state, int flag, int arg2) {
+#if BFS_CAN_CHECK_CAPABILITIES
+ struct expr *expr = parse_nullary_test(state, eval_capable);
+ if (expr) {
+ expr->cost = STAT_COST;
+ expr->probability = 0.000002;
+ }
+ return expr;
+#else
+ parse_error(state, "%s is missing platform support.\n", state->argv[0]);
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -(no)?color.
+ */
+static struct expr *parse_color(struct parser_state *state, int color, int arg2) {
+ struct cmdline *cmdline = state->cmdline;
+ struct colors *colors = cmdline->colors;
+ if (color) {
+ state->use_color = COLOR_ALWAYS;
+ cmdline->cout->colors = colors;
+ cmdline->cerr->colors = colors;
+ } else {
+ state->use_color = COLOR_NEVER;
+ cmdline->cout->colors = NULL;
+ cmdline->cerr->colors = NULL;
+ }
+ return parse_nullary_option(state);
+}
+
+/**
+ * Parse -{false,true}.
+ */
+static struct expr *parse_const(struct parser_state *state, int value, int arg2) {
+ parser_advance(state, T_TEST, 1);
+ return value ? &expr_true : &expr_false;
+}
+
+/**
+ * Parse -daystart.
+ */
+static struct expr *parse_daystart(struct parser_state *state, int arg1, int arg2) {
+ struct tm tm;
+ if (xlocaltime(&state->now.tv_sec, &tm) != 0) {
+ perror("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 = mktime(&tm);
+ if (time == -1) {
+ perror("mktime()");
+ return NULL;
+ }
+
+ state->now.tv_sec = time;
+ state->now.tv_nsec = 0;
+
+ return parse_nullary_positional_option(state);
+}
+
+/**
+ * Parse -delete.
+ */
+static struct expr *parse_delete(struct parser_state *state, int arg1, int arg2) {
+ state->cmdline->flags |= BFTW_DEPTH;
+ state->depth_arg = state->argv[0];
+ return parse_nullary_action(state, eval_delete);
+}
+
+/**
+ * Parse -d.
+ */
+static struct expr *parse_depth(struct parser_state *state, int arg1, int arg2) {
+ state->cmdline->flags |= BFTW_DEPTH;
+ state->depth_arg = state->argv[0];
+ return parse_nullary_flag(state);
+}
+
+/**
+ * Parse -depth [N].
+ */
+static struct 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 expr *parse_depth_limit(struct parser_state *state, int is_min, int arg2) {
+ struct cmdline *cmdline = state->cmdline;
+ const char *arg = state->argv[0];
+ const char *value = state->argv[1];
+ if (!value) {
+ parse_error(state, "%s needs a value.\n", arg);
+ return NULL;
+ }
+
+ int *depth = is_min ? &cmdline->mindepth : &cmdline->maxdepth;
+ if (!parse_int(state, value, depth, IF_INT | IF_UNSIGNED)) {
+ return NULL;
+ }
+
+ return parse_unary_option(state);
+}
+
+/**
+ * Parse -empty.
+ */
+static struct expr *parse_empty(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_test(state, eval_empty);
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->cost = 2000.0;
+ expr->probability = 0.01;
+
+ if (state->cmdline->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 expr *parse_exec(struct parser_state *state, int flags, int arg2) {
+ struct bfs_exec *execbuf = parse_bfs_exec(state->argv, flags, state->cmdline);
+ if (!execbuf) {
+ return NULL;
+ }
+
+ struct expr *expr = parse_action(state, eval_exec, execbuf->tmpl_argc + 2);
+ if (!expr) {
+ free_bfs_exec(execbuf);
+ return NULL;
+ }
+
+ expr->execbuf = 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;
+ }
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -exit [STATUS].
+ */
+static struct 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, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) {
+ argc = 2;
+ }
+
+ struct expr *expr = parse_action(state, eval_exit, argc);
+ if (expr) {
+ expr_set_never_returns(expr);
+ expr->idata = status;
+ }
+ return expr;
+}
+
+/**
+ * Parse -f PATH.
+ */
+static struct expr *parse_f(struct parser_state *state, int arg1, int arg2) {
+ parser_advance(state, T_FLAG, 1);
+
+ const char *path = state->argv[0];
+ if (!path) {
+ parse_error(state, "-f requires a path.\n");
+ return NULL;
+ }
+
+ if (parse_root(state, path) != 0) {
+ return NULL;
+ }
+
+ parser_advance(state, T_PATH, 1);
+ return &expr_true;
+}
+
+/**
+ * Parse -fls FILE.
+ */
+static struct expr *parse_fls(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_unary_action(state, eval_fls);
+ if (expr) {
+ expr_set_always_true(expr);
+ expr->cost = PRINT_COST;
+ if (expr_open(state, expr, expr->sdata) != 0) {
+ goto fail;
+ }
+ expr->reftime = state->now;
+ }
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -fprint FILE.
+ */
+static struct expr *parse_fprint(struct parser_state *state, int arg1, int arg2) {
+ struct 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->sdata) != 0) {
+ goto fail;
+ }
+ }
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -fprint0 FILE.
+ */
+static struct expr *parse_fprint0(struct parser_state *state, int arg1, int arg2) {
+ struct 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->sdata) != 0) {
+ goto fail;
+ }
+ }
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -fprintf FILE FORMAT.
+ */
+static struct 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, "%s needs a file.\n", arg);
+ return NULL;
+ }
+
+ const char *format = state->argv[2];
+ if (!format) {
+ parse_error(state, "%s needs a format string.\n", arg);
+ return NULL;
+ }
+
+ struct 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;
+ }
+
+ expr->printf = parse_bfs_printf(format, state->cmdline);
+ if (!expr->printf) {
+ goto fail;
+ }
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -fstype TYPE.
+ */
+static struct expr *parse_fstype(struct parser_state *state, int arg1, int arg2) {
+ struct cmdline *cmdline = state->cmdline;
+ if (!cmdline->mtab) {
+ parse_error(state, "Couldn't parse the mount table: %s.\n", strerror(cmdline->mtab_error));
+ return NULL;
+ }
+
+ struct expr *expr = parse_unary_test(state, eval_fstype);
+ if (expr) {
+ expr->cost = STAT_COST;
+ }
+ return expr;
+}
+
+/**
+ * Parse -gid/-group.
+ */
+static struct expr *parse_group(struct parser_state *state, int arg1, int arg2) {
+ const char *arg = state->argv[0];
+
+ struct expr *expr = parse_unary_test(state, eval_gid);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct group *grp = getgrnam(expr->sdata);
+ if (grp) {
+ expr->idata = grp->gr_gid;
+ expr->cmp_flag = CMP_EXACT;
+ } else if (looks_like_icmp(expr->sdata)) {
+ if (!parse_icmp(state, expr->sdata, expr, 0)) {
+ goto fail;
+ }
+ } else {
+ parse_error(state, "%s %s: No such group.\n", arg, expr->sdata);
+ goto fail;
+ }
+
+ expr->cost = STAT_COST;
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -unique.
+ */
+static struct expr *parse_unique(struct parser_state *state, int arg1, int arg2) {
+ state->cmdline->unique = true;
+ return parse_nullary_option(state);
+}
+
+/**
+ * Parse -used N.
+ */
+static struct expr *parse_used(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_test_icmp(state, eval_used);
+ if (expr) {
+ expr->cost = STAT_COST;
+ }
+ return expr;
+}
+
+/**
+ * Parse -uid/-user.
+ */
+static struct expr *parse_user(struct parser_state *state, int arg1, int arg2) {
+ const char *arg = state->argv[0];
+
+ struct expr *expr = parse_unary_test(state, eval_uid);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct passwd *pwd = getpwnam(expr->sdata);
+ if (pwd) {
+ expr->idata = pwd->pw_uid;
+ expr->cmp_flag = CMP_EXACT;
+ } else if (looks_like_icmp(expr->sdata)) {
+ if (!parse_icmp(state, expr->sdata, expr, 0)) {
+ goto fail;
+ }
+ } else {
+ parse_error(state, "%s %s: No such user.\n", arg, expr->sdata);
+ goto fail;
+ }
+
+ expr->cost = STAT_COST;
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -hidden.
+ */
+static struct expr *parse_hidden(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_test(state, eval_hidden);
+ if (expr) {
+ expr->probability = 0.01;
+ }
+ return expr;
+}
+
+/**
+ * Parse -(no)?ignore_readdir_race.
+ */
+static struct expr *parse_ignore_races(struct parser_state *state, int ignore, int arg2) {
+ state->cmdline->ignore_races = ignore;
+ return parse_nullary_option(state);
+}
+
+/**
+ * Parse -inum N.
+ */
+static struct expr *parse_inum(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_test_icmp(state, eval_inum);
+ if (expr) {
+ expr->cost = STAT_COST;
+ expr->probability = expr->cmp_flag == CMP_EXACT ? 0.01 : 0.50;
+ }
+ return expr;
+}
+
+/**
+ * Parse -links N.
+ */
+static struct expr *parse_links(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_test_icmp(state, eval_links);
+ if (expr) {
+ expr->cost = STAT_COST;
+ expr->probability = expr_cmp(expr, 1) ? 0.99 : 0.01;
+ }
+ return expr;
+}
+
+/**
+ * Parse -ls.
+ */
+static struct expr *parse_ls(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_action(state, eval_fls);
+ if (expr) {
+ init_print_expr(state, expr);
+ expr->reftime = state->now;
+ }
+ return expr;
+}
+
+/**
+ * Parse -mount, -xdev.
+ */
+static struct expr *parse_mount(struct parser_state *state, int arg1, int arg2) {
+ state->cmdline->flags |= BFTW_XDEV;
+ return parse_nullary_option(state);
+}
+
+/**
+ * Common code for fnmatch() tests.
+ */
+static struct expr *parse_fnmatch(const struct parser_state *state, struct expr *expr, bool casefold) {
+ if (!expr) {
+ return NULL;
+ }
+
+ if (casefold) {
+#ifdef FNM_CASEFOLD
+ expr->idata = FNM_CASEFOLD;
+#else
+ parse_error(state, "%s is missing platform support.\n", expr->argv[0]);
+ free_expr(expr);
+ return NULL;
+#endif
+ } else {
+ expr->idata = 0;
+ }
+
+ expr->cost = 400.0;
+
+ if (strchr(expr->sdata, '*')) {
+ expr->probability = 0.5;
+ } else {
+ expr->probability = 0.1;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -i?name.
+ */
+static struct expr *parse_name(struct parser_state *state, int casefold, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_name);
+ return parse_fnmatch(state, expr, casefold);
+}
+
+/**
+ * Parse -i?path, -i?wholename.
+ */
+static struct expr *parse_path(struct parser_state *state, int casefold, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_path);
+ return parse_fnmatch(state, expr, casefold);
+}
+
+/**
+ * Parse -i?lname.
+ */
+static struct expr *parse_lname(struct parser_state *state, int casefold, int arg2) {
+ struct 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 -newerXY.
+ */
+static struct 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 -newerXY; found %s.\n", arg);
+ return NULL;
+ }
+
+ struct 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_error(state, "%s: For -newerXY, X should be 'a', 'c', 'm', or 'B'.\n", arg);
+ goto fail;
+ }
+
+ if (arg[7] == 't') {
+ parse_error(state, "%s: Explicit reference times ('t') are not supported.\n", arg);
+ goto fail;
+ } else {
+ enum bfs_stat_field field = parse_newerxy_field(arg[7]);
+ if (!field) {
+ parse_error(state, "%s: For -newerXY, Y should be 'a', 'c', 'm', 'B', or 't'.\n", arg);
+ goto fail;
+ }
+
+ struct bfs_stat sb;
+ if (stat_arg(state, expr, &sb) != 0) {
+ goto fail;
+ }
+
+
+ const struct timespec *reftime = bfs_stat_time(&sb, field);
+ if (!reftime) {
+ parse_error(state, "'%s': Couldn't get file %s.\n", expr->sdata, bfs_stat_field_name(field));
+ goto fail;
+ }
+
+ expr->reftime = *reftime;
+ }
+
+ expr->cost = STAT_COST;
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -nogroup.
+ */
+static struct expr *parse_nogroup(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_test(state, eval_nogroup);
+ if (expr) {
+ expr->cost = 9000.0;
+ expr->probability = 0.01;
+ expr->ephemeral_fds = 1;
+ }
+ return expr;
+}
+
+/**
+ * Parse -nohidden.
+ */
+static struct expr *parse_nohidden(struct parser_state *state, int arg1, int arg2) {
+ state->prune_arg = state->argv[0];
+ return parse_nullary_action(state, eval_nohidden);
+}
+
+/**
+ * Parse -noleaf.
+ */
+static struct expr *parse_noleaf(struct parser_state *state, int arg1, int arg2) {
+ if (state->warn) {
+ parse_warning(state, "bfs does not apply the optimization that %s inhibits.\n\n", state->argv[0]);
+ }
+
+ return parse_nullary_option(state);
+}
+
+/**
+ * Parse -nouser.
+ */
+static struct expr *parse_nouser(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_test(state, eval_nouser);
+ if (expr) {
+ expr->cost = 9000.0;
+ expr->probability = 0.01;
+ expr->ephemeral_fds = 1;
+ }
+ return expr;
+}
+
+/**
+ * Parse a permission mode like chmod(1).
+ */
+static int parse_mode(const struct parser_state *state, const char *mode, struct expr *expr) {
+ if (mode[0] >= '0' && mode[0] <= '9') {
+ unsigned int parsed;
+ if (!parse_int(state, 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;
+ // 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;
+ // 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;
+ }
+ // 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;
+ // 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':
+ file_change |= S_ISVTX;
+ dir_change |= S_ISVTX;
+ break;
+ default:
+ mstate = MODE_ACTION_APPLY;
+ continue;
+ }
+ break;
+ }
+
+ ++i;
+ }
+
+done:
+ return 0;
+
+fail:
+ parse_error(state, "'%s' is an invalid mode.\n", mode);
+ return -1;
+}
+
+/**
+ * Parse -perm MODE.
+ */
+static struct expr *parse_perm(struct parser_state *state, int field, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_perm);
+ if (!expr) {
+ return NULL;
+ }
+
+ const char *mode = expr->sdata;
+ switch (mode[0]) {
+ case '-':
+ expr->mode_cmp = MODE_ALL;
+ ++mode;
+ break;
+ case '/':
+ expr->mode_cmp = MODE_ANY;
+ ++mode;
+ break;
+ case '+':
+ if (mode[1] >= '0' && mode[1] <= '9') {
+ expr->mode_cmp = MODE_ANY;
+ ++mode;
+ break;
+ }
+ // Fallthrough
+ default:
+ expr->mode_cmp = MODE_EXACT;
+ break;
+ }
+
+ if (parse_mode(state, mode, expr) != 0) {
+ goto fail;
+ }
+
+ expr->cost = STAT_COST;
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -print.
+ */
+static struct expr *parse_print(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_action(state, eval_fprint);
+ if (expr) {
+ init_print_expr(state, expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -print0.
+ */
+static struct expr *parse_print0(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_action(state, eval_fprint0);
+ if (expr) {
+ init_print_expr(state, expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -printf FORMAT.
+ */
+static struct expr *parse_printf(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_unary_action(state, eval_fprintf);
+ if (!expr) {
+ return NULL;
+ }
+
+ init_print_expr(state, expr);
+
+ expr->printf = parse_bfs_printf(expr->sdata, state->cmdline);
+ if (!expr->printf) {
+ free_expr(expr);
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -printx.
+ */
+static struct expr *parse_printx(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_action(state, eval_fprintx);
+ if (expr) {
+ init_print_expr(state, expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -prune.
+ */
+static struct expr *parse_prune(struct parser_state *state, int arg1, int arg2) {
+ state->prune_arg = state->argv[0];
+
+ struct expr *expr = parse_nullary_action(state, eval_prune);
+ if (expr) {
+ expr_set_always_true(expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -quit.
+ */
+static struct expr *parse_quit(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_action(state, eval_quit);
+ if (expr) {
+ expr_set_never_returns(expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -i?regex.
+ */
+static struct expr *parse_regex(struct parser_state *state, int flags, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_regex);
+ if (!expr) {
+ goto fail;
+ }
+
+ expr->regex = malloc(sizeof(regex_t));
+ if (!expr->regex) {
+ perror("malloc()");
+ goto fail;
+ }
+
+ int err = regcomp(expr->regex, expr->sdata, state->regex_flags | flags);
+ if (err != 0) {
+ char *str = xregerror(err, expr->regex);
+ if (str) {
+ parse_error(state, "%s %s: %s.\n", expr->argv[0], expr->argv[1], str);
+ free(str);
+ } else {
+ perror("xregerror()");
+ }
+ goto fail_regex;
+ }
+
+ return expr;
+
+fail_regex:
+ free(expr->regex);
+ expr->regex = NULL;
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -E.
+ */
+static struct expr *parse_regex_extended(struct parser_state *state, int arg1, int arg2) {
+ state->regex_flags = REG_EXTENDED;
+ return parse_nullary_flag(state);
+}
+
+/**
+ * Parse -regextype TYPE.
+ */
+static struct expr *parse_regextype(struct parser_state *state, int arg1, int arg2) {
+ const char *type;
+ struct expr *expr = parse_unary_positional_option(state, &type);
+ if (!expr) {
+ goto fail;
+ }
+
+ FILE *file = stderr;
+
+ if (strcmp(type, "posix-basic") == 0) {
+ state->regex_flags = 0;
+ } else if (strcmp(type, "posix-extended") == 0) {
+ state->regex_flags = REG_EXTENDED;
+ } else if (strcmp(type, "help") == 0) {
+ state->just_info = true;
+ file = stdout;
+ goto fail_list_types;
+ } else {
+ goto fail_bad_type;
+ }
+
+ return expr;
+
+fail_bad_type:
+ parse_error(state, "Unsupported -regextype '%s'.\n\n", type);
+fail_list_types:
+ fputs("Supported types are:\n\n", file);
+ fputs(" posix-basic: POSIX basic regular expressions (BRE)\n", file);
+ fputs(" posix-extended: POSIX extended regular expressions (ERE)\n", file);
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -samefile FILE.
+ */
+static struct expr *parse_samefile(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_samefile);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_stat sb;
+ if (stat_arg(state, expr, &sb) != 0) {
+ free_expr(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 expr *parse_search_strategy(struct parser_state *state, int arg1, int arg2) {
+ const char *flag = state->argv[0];
+ const char *arg = state->argv[1];
+ if (!arg) {
+ parse_error(state, "%s needs an argument.\n", flag);
+ return NULL;
+ }
+
+ struct cmdline *cmdline = state->cmdline;
+ FILE *file = stderr;
+
+ if (strcmp(arg, "bfs") == 0) {
+ cmdline->strategy = BFTW_BFS;
+ } else if (strcmp(arg, "dfs") == 0) {
+ cmdline->strategy = BFTW_DFS;
+ } else if (strcmp(arg, "ids") == 0) {
+ cmdline->strategy = BFTW_IDS;
+ } else if (strcmp(arg, "help") == 0) {
+ state->just_info = true;
+ file = stdout;
+ goto fail_list_strategies;
+ } else {
+ goto fail_bad_strategy;
+ }
+
+ return parse_unary_flag(state);
+
+fail_bad_strategy:
+ parse_error(state, "Unrecognized search strategy '%s'.\n\n", arg);
+fail_list_strategies:
+ fputs("Supported search strategies:\n\n", file);
+ fputs(" bfs: breadth-first search\n", file);
+ fputs(" dfs: depth-first search\n", file);
+ fputs(" ids: iterative deepening search\n", file);
+ return NULL;
+}
+
+/**
+ * Parse -size N[cwbkMGTP]?.
+ */
+static struct expr *parse_size(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_size);
+ if (!expr) {
+ return NULL;
+ }
+
+ const char *unit = parse_icmp(state, expr->sdata, expr, IF_PARTIAL_OK);
+ if (!unit) {
+ goto fail;
+ }
+
+ if (strlen(unit) > 1) {
+ goto bad_unit;
+ }
+
+ switch (*unit) {
+ case '\0':
+ case 'b':
+ expr->size_unit = SIZE_BLOCKS;
+ break;
+ case 'c':
+ expr->size_unit = SIZE_BYTES;
+ break;
+ case 'w':
+ expr->size_unit = SIZE_WORDS;
+ break;
+ case 'k':
+ expr->size_unit = SIZE_KB;
+ break;
+ case 'M':
+ expr->size_unit = SIZE_MB;
+ break;
+ case 'G':
+ expr->size_unit = SIZE_GB;
+ break;
+ case 'T':
+ expr->size_unit = SIZE_TB;
+ break;
+ case 'P':
+ expr->size_unit = SIZE_PB;
+ break;
+
+ default:
+ goto bad_unit;
+ }
+
+ expr->cost = STAT_COST;
+ expr->probability = expr->cmp_flag == CMP_EXACT ? 0.01 : 0.50;
+
+ return expr;
+
+bad_unit:
+ parse_error(state, "%s %s: Expected a size unit (one of cwbkMGTP); found '%s'.\n",
+ expr->argv[0], expr->argv[1], unit);
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -sparse.
+ */
+static struct expr *parse_sparse(struct parser_state *state, int arg1, int arg2) {
+ struct expr *expr = parse_nullary_test(state, eval_sparse);
+ if (expr) {
+ expr->cost = STAT_COST;
+ }
+ return expr;
+}
+
+/**
+ * Parse -x?type [bcdpflsD].
+ */
+static struct expr *parse_type(struct parser_state *state, int x, int arg2) {
+ eval_fn *eval = x ? eval_xtype : eval_type;
+ struct expr *expr = parse_unary_test(state, eval);
+ if (!expr) {
+ return NULL;
+ }
+
+ enum bftw_typeflag types = 0;
+ double probability = 0.0;
+
+ const char *c = expr->sdata;
+ while (true) {
+ enum bftw_typeflag type;
+ double type_prob;
+
+ switch (*c) {
+ case 'b':
+ type = BFTW_BLK;
+ type_prob = 0.00000721183;
+ break;
+ case 'c':
+ type = BFTW_CHR;
+ type_prob = 0.0000499855;
+ break;
+ case 'd':
+ type = BFTW_DIR;
+ type_prob = 0.114475;
+ break;
+ case 'D':
+ type = BFTW_DOOR;
+ type_prob = 0.000001;
+ break;
+ case 'p':
+ type = BFTW_FIFO;
+ type_prob = 0.00000248684;
+ break;
+ case 'f':
+ type = BFTW_REG;
+ type_prob = 0.859772;
+ break;
+ case 'l':
+ type = BFTW_LNK;
+ type_prob = 0.0256816;
+ break;
+ case 's':
+ type = BFTW_SOCK;
+ type_prob = 0.0000116881;
+ break;
+ case 'w':
+ type = BFTW_WHT;
+ type_prob = 0.000001;
+ break;
+
+ case '\0':
+ parse_error(state, "%s %s: Expected a type flag.\n", expr->argv[0], expr->argv[1]);
+ goto fail;
+
+ default:
+ parse_error(state, "%s %s: Unknown type flag '%c' (expected one of [bcdpflsD]).\n",
+ expr->argv[0], expr->argv[1], *c);
+ goto fail;
+ }
+
+ if (!(types & type)) {
+ types |= type;
+ probability += type_prob;
+ }
+
+ ++c;
+ if (*c == '\0') {
+ break;
+ } else if (*c == ',') {
+ ++c;
+ continue;
+ } else {
+ parse_error(state, "%s %s: Types must be comma-separated.\n", expr->argv[0], expr->argv[1]);
+ goto fail;
+ }
+ }
+
+ expr->idata = types;
+ expr->probability = probability;
+
+ if (x && state->cmdline->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:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Parse -(no)?warn.
+ */
+static struct expr *parse_warn(struct parser_state *state, int warn, int arg2) {
+ state->warn = warn;
+ return parse_nullary_positional_option(state);
+}
+
+/**
+ * Parse -xattr.
+ */
+static struct expr *parse_xattr(struct parser_state *state, int arg1, int arg2) {
+#if BFS_CAN_CHECK_XATTRS
+ struct expr *expr = parse_nullary_test(state, eval_xattr);
+ if (expr) {
+ expr->cost = STAT_COST;
+ expr->probability = 0.01;
+ }
+ return expr;
+#else
+ parse_error(state, "%s is missing platform support.\n", state->argv[0]);
+ return NULL;
+#endif
+}
+
+/**
+ * Launch a pager for the help output.
+ */
+static CFILE *launch_pager(pid_t *pid, CFILE *cout) {
+ char *pager = getenv("PAGER");
+ if (!pager) {
+ pager = "more";
+ }
+
+ int pipefd[2];
+ if (pipe(pipefd) != 0) {
+ goto fail;
+ }
+
+ FILE *file = fdopen(pipefd[1], "w");
+ if (!file) {
+ goto fail_pipe;
+ }
+ pipefd[1] = -1;
+
+ CFILE *ret = cfdup(file, NULL);
+ if (!ret) {
+ goto fail_file;
+ }
+ file = NULL;
+ ret->close = true;
+ ret->colors = cout->colors;
+
+ struct bfs_spawn ctx;
+ if (bfs_spawn_init(&ctx) != 0) {
+ goto fail_ret;
+ }
+
+ if (bfs_spawn_setflags(&ctx, BFS_SPAWN_USEPATH) != 0) {
+ goto fail_ctx;
+ }
+
+ 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[] = {
+ pager,
+ NULL,
+ };
+
+ extern char **environ;
+ char **envp = environ;
+
+ if (!getenv("LESS")) {
+ size_t envc;
+ for (envc = 0; environ[envc]; ++envc);
+ ++envc;
+
+ envp = malloc((envc + 1)*sizeof(*envp));
+ if (!envp) {
+ goto fail_ctx;
+ }
+
+ memcpy(envp, environ, (envc - 1)*sizeof(*envp));
+ envp[envc - 1] = "LESS=FKRX";
+ envp[envc] = NULL;
+ }
+
+ *pid = bfs_spawn(pager, &ctx, argv, envp);
+ if (*pid < 0) {
+ goto fail_envp;
+ }
+
+ close(pipefd[0]);
+ if (envp != environ) {
+ free(envp);
+ }
+ bfs_spawn_destroy(&ctx);
+ return ret;
+
+fail_envp:
+ if (envp != environ) {
+ free(envp);
+ }
+fail_ctx:
+ bfs_spawn_destroy(&ctx);
+fail_ret:
+ cfclose(ret);
+fail_file:
+ if (file) {
+ fclose(file);
+ }
+fail_pipe:
+ if (pipefd[1] >= 0) {
+ close(pipefd[1]);
+ }
+ if (pipefd[0] >= 0) {
+ close(pipefd[0]);
+ }
+fail:
+ return cout;
+}
+
+/**
+ * "Parse" -help.
+ */
+static struct expr *parse_help(struct parser_state *state, int arg1, int arg2) {
+ CFILE *cout = state->cmdline->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}-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${rs}${bld}N${rs}\n");
+ cfprintf(cout, " Enable optimization level ${bld}N${rs} (default: 3)\n");
+ cfprintf(cout, " ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${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}d${rs}eepening ${bld}s${rs}earch (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}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}-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, " ${blu}-xdev${rs}\n");
+ cfprintf(cout, " Don't descend into other mount points\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}-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\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}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, " ${blu}-nohidden${rs}\n");
+ cfprintf(cout, " Find hidden files, or filter them out\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${rs}${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}].\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}-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");
+#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}FORMAT${rs} ${bld}FILE${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}-nohidden${rs}\n");
+ cfprintf(cout, " Filter out hidden files and directories\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 expr *parse_version(struct parser_state *state, int arg1, int arg2) {
+ cfprintf(state->cmdline->cout, "${ex}bfs${rs} ${bld}%s${rs}\n\n", BFS_VERSION);
+
+ printf("%s\n", BFS_HOMEPAGE);
+
+ state->just_info = true;
+ return NULL;
+}
+
+typedef struct expr *parse_fn(struct parser_state *state, int arg1, int arg2);
+
+/**
+ * An entry in the parse table for literals.
+ */
+struct table_entry {
+ char *arg;
+ parse_fn *parse;
+ int arg1;
+ int arg2;
+ bool prefix;
+};
+
+/**
+ * The parse table for literals.
+ */
+static const struct table_entry parse_table[] = {
+ {"--"},
+ {"--help", parse_help},
+ {"--version", parse_version},
+ {"-Bmin", parse_time, BFS_STAT_BTIME, MINUTES},
+ {"-Bnewer", parse_newer, BFS_STAT_BTIME},
+ {"-Btime", parse_time, BFS_STAT_BTIME, DAYS},
+ {"-D", parse_debug},
+ {"-E", parse_regex_extended},
+ {"-H", parse_follow, BFTW_COMFOLLOW, false},
+ {"-L", parse_follow, BFTW_LOGICAL, false},
+ {"-O", parse_optlevel, 0, 0, true},
+ {"-P", parse_follow, 0, false},
+ {"-S", parse_search_strategy},
+ {"-X", parse_xargs_safe},
+ {"-a"},
+ {"-acl", parse_acl},
+ {"-amin", parse_time, BFS_STAT_ATIME, MINUTES},
+ {"-and"},
+ {"-anewer", parse_newer, BFS_STAT_ATIME},
+ {"-atime", parse_time, BFS_STAT_ATIME, DAYS},
+ {"-capable", parse_capable},
+ {"-cmin", parse_time, BFS_STAT_CTIME, MINUTES},
+ {"-cnewer", parse_newer, BFS_STAT_CTIME},
+ {"-color", parse_color, true},
+ {"-ctime", parse_time, BFS_STAT_CTIME, DAYS},
+ {"-d", parse_depth},
+ {"-daystart", parse_daystart},
+ {"-delete", parse_delete},
+ {"-depth", parse_depth_n},
+ {"-empty", parse_empty},
+ {"-exec", parse_exec, 0},
+ {"-execdir", parse_exec, BFS_EXEC_CHDIR},
+ {"-executable", parse_access, X_OK},
+ {"-exit", parse_exit},
+ {"-f", parse_f},
+ {"-false", parse_const, false},
+ {"-fls", parse_fls},
+ {"-follow", parse_follow, BFTW_LOGICAL, true},
+ {"-fprint", parse_fprint},
+ {"-fprint0", parse_fprint0},
+ {"-fprintf", parse_fprintf},
+ {"-fstype", parse_fstype},
+ {"-gid", parse_group},
+ {"-group", parse_group},
+ {"-help", parse_help},
+ {"-hidden", parse_hidden},
+ {"-ignore_readdir_race", parse_ignore_races, true},
+ {"-ilname", parse_lname, true},
+ {"-iname", parse_name, true},
+ {"-inum", parse_inum},
+ {"-ipath", parse_path, true},
+ {"-iregex", parse_regex, REG_ICASE},
+ {"-iwholename", parse_path, true},
+ {"-links", parse_links},
+ {"-lname", parse_lname, false},
+ {"-ls", parse_ls},
+ {"-maxdepth", parse_depth_limit, false},
+ {"-mindepth", parse_depth_limit, true},
+ {"-mmin", parse_time, BFS_STAT_MTIME, MINUTES},
+ {"-mnewer", parse_newer, BFS_STAT_MTIME},
+ {"-mount", parse_mount},
+ {"-mtime", parse_time, BFS_STAT_MTIME, DAYS},
+ {"-name", parse_name, false},
+ {"-newer", parse_newer, BFS_STAT_MTIME},
+ {"-newer", parse_newerxy, 0, 0, true},
+ {"-nocolor", parse_color, false},
+ {"-nogroup", parse_nogroup},
+ {"-nohidden", parse_nohidden},
+ {"-noignore_readdir_race", parse_ignore_races, false},
+ {"-noleaf", parse_noleaf},
+ {"-not"},
+ {"-nouser", parse_nouser},
+ {"-nowarn", parse_warn, false},
+ {"-o"},
+ {"-ok", parse_exec, BFS_EXEC_CONFIRM},
+ {"-okdir", parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR},
+ {"-or"},
+ {"-path", parse_path, false},
+ {"-perm", parse_perm},
+ {"-print", parse_print},
+ {"-print0", parse_print0},
+ {"-printf", parse_printf},
+ {"-printx", parse_printx},
+ {"-prune", parse_prune},
+ {"-quit", parse_quit},
+ {"-readable", parse_access, R_OK},
+ {"-regex", parse_regex, 0},
+ {"-regextype", parse_regextype},
+ {"-rm", parse_delete},
+ {"-samefile", parse_samefile},
+ {"-size", parse_size},
+ {"-sparse", parse_sparse},
+ {"-true", parse_const, true},
+ {"-type", parse_type, false},
+ {"-uid", parse_user},
+ {"-unique", parse_unique},
+ {"-used", parse_used},
+ {"-user", parse_user},
+ {"-version", parse_version},
+ {"-warn", parse_warn, true},
+ {"-wholename", parse_path, false},
+ {"-writable", parse_access, W_OK},
+ {"-x", parse_mount},
+ {"-xattr", parse_xattr},
+ {"-xdev", parse_mount},
+ {"-xtype", 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 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);
+
+ parse_error(state, "Unknown argument '%s'; did you mean '%s'?", arg, match->arg);
+
+ 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; found '%s'.\n", arg);
+ return NULL;
+}
+
+/**
+ * FACTOR : "(" EXPR ")"
+ * | "!" FACTOR | "-not" FACTOR
+ * | LITERAL
+ */
+static struct expr *parse_factor(struct parser_state *state) {
+ if (skip_paths(state) != 0) {
+ return NULL;
+ }
+
+ const char *arg = state->argv[0];
+ if (!arg) {
+ parse_error(state, "Expression terminated prematurely after '%s'.\n", state->last_arg);
+ return NULL;
+ }
+
+ if (strcmp(arg, "(") == 0) {
+ parser_advance(state, T_OPERATOR, 1);
+
+ struct expr *expr = parse_expr(state);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (skip_paths(state) != 0) {
+ free_expr(expr);
+ return NULL;
+ }
+
+ arg = state->argv[0];
+ if (!arg || strcmp(arg, ")") != 0) {
+ parse_error(state, "Expected a ')' after '%s'.\n", state->argv[-1]);
+ free_expr(expr);
+ return NULL;
+ }
+ parser_advance(state, T_OPERATOR, 1);
+
+ return expr;
+ } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) {
+ char **argv = parser_advance(state, T_OPERATOR, 1);
+
+ struct 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 expr *parse_term(struct parser_state *state) {
+ struct expr *term = parse_factor(state);
+
+ while (term) {
+ if (skip_paths(state) != 0) {
+ free_expr(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 expr *lhs = term;
+ struct expr *rhs = parse_factor(state);
+ if (!rhs) {
+ free_expr(lhs);
+ return NULL;
+ }
+
+ term = new_binary_expr(eval_and, lhs, rhs, argv);
+ }
+
+ return term;
+}
+
+/**
+ * CLAUSE : TERM
+ * | CLAUSE "-o" TERM
+ * | CLAUSE "-or" TERM
+ */
+static struct expr *parse_clause(struct parser_state *state) {
+ struct expr *clause = parse_term(state);
+
+ while (clause) {
+ if (skip_paths(state) != 0) {
+ free_expr(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 expr *lhs = clause;
+ struct expr *rhs = parse_term(state);
+ if (!rhs) {
+ free_expr(lhs);
+ return NULL;
+ }
+
+ clause = new_binary_expr(eval_or, lhs, rhs, argv);
+ }
+
+ return clause;
+}
+
+/**
+ * EXPR : CLAUSE
+ * | EXPR "," CLAUSE
+ */
+static struct expr *parse_expr(struct parser_state *state) {
+ struct expr *expr = parse_clause(state);
+
+ while (expr) {
+ if (skip_paths(state) != 0) {
+ free_expr(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 expr *lhs = expr;
+ struct expr *rhs = parse_clause(state);
+ if (!rhs) {
+ free_expr(lhs);
+ return NULL;
+ }
+
+ expr = new_binary_expr(eval_comma, lhs, rhs, argv);
+ }
+
+ return expr;
+}
+
+/**
+ * Parse the top-level expression.
+ */
+static struct expr *parse_whole_expr(struct parser_state *state) {
+ if (skip_paths(state) != 0) {
+ return NULL;
+ }
+
+ struct expr *expr = &expr_true;
+ if (state->argv[0]) {
+ expr = parse_expr(state);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ if (state->argv[0]) {
+ parse_error(state, "Unexpected argument '%s'.\n", state->argv[0]);
+ goto fail;
+ }
+
+ if (state->implicit_print) {
+ struct expr *print = new_expr(eval_fprint, 1, &fake_print_arg);
+ if (!print) {
+ goto fail;
+ }
+ init_print_expr(state, print);
+
+ expr = new_binary_expr(eval_and, expr, print, &fake_and_arg);
+ if (!expr) {
+ goto fail;
+ }
+ }
+
+ if (state->warn && state->depth_arg && state->prune_arg) {
+ parse_warning(state, "%s does not work in the presence of %s.\n", state->prune_arg, state->depth_arg);
+
+ if (state->interactive) {
+ fprintf(stderr, "Do you want to continue? ");
+ if (ynprompt() == 0) {
+ goto fail;
+ }
+ }
+
+ fprintf(stderr, "\n");
+ }
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
+ * Dump the parsed form of the command line, for debugging.
+ */
+void dump_cmdline(const struct cmdline *cmdline, bool verbose) {
+ CFILE *cerr = cmdline->cerr;
+
+ cfprintf(cerr, "${ex}%s${rs} ", cmdline->argv[0]);
+
+ const char *strategy = NULL;
+ switch (cmdline->strategy) {
+ case BFTW_BFS:
+ strategy = "bfs";
+ break;
+ case BFTW_DFS:
+ strategy = "dfs";
+ break;
+ case BFTW_IDS:
+ strategy = "ids";
+ break;
+ }
+ assert(strategy);
+ cfprintf(cerr, "${cyn}-S${rs} ${bld}%s${rs} ", strategy);
+
+ if (cmdline->flags & BFTW_LOGICAL) {
+ cfprintf(cerr, "${cyn}-L${rs} ");
+ } else if (cmdline->flags & BFTW_COMFOLLOW) {
+ cfprintf(cerr, "${cyn}-H${rs} ");
+ } else {
+ cfprintf(cerr, "${cyn}-P${rs} ");
+ }
+
+ if (cmdline->optlevel != 3) {
+ cfprintf(cerr, "${cyn}-O%d${rs} ", cmdline->optlevel);
+ }
+
+ enum debug_flags debug = cmdline->debug;
+ if (debug) {
+ cfprintf(cerr, "${cyn}-D${rs} ");
+ for (int i = 0; debug; ++i) {
+ enum debug_flags flag = debug_flags[i].flag;
+ const char *name = debug_flags[i].name;
+ if ((debug & flag) == flag) {
+ cfprintf(cerr, "${bld}%s${rs}", name);
+ debug ^= flag;
+ if (debug) {
+ cfprintf(cerr, ",");
+ }
+ }
+ }
+ cfprintf(cerr, " ");
+ }
+
+ for (size_t i = 0; i < cmdline->npaths; ++i) {
+ const char *path = cmdline->paths[i];
+ char c = path[0];
+ if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') {
+ cfprintf(cerr, "${cyn}-f${rs} ");
+ }
+ cfprintf(cerr, "${mag}%s${rs} ", path);
+ }
+
+ if (cmdline->cout->colors) {
+ cfprintf(cerr, "${blu}-color${rs} ");
+ } else {
+ cfprintf(cerr, "${blu}-nocolor${rs} ");
+ }
+ if (cmdline->flags & BFTW_DEPTH) {
+ cfprintf(cerr, "${blu}-depth${rs} ");
+ }
+ if (cmdline->ignore_races) {
+ cfprintf(cerr, "${blu}-ignore_readdir_race${rs} ");
+ }
+ if (cmdline->flags & BFTW_XDEV) {
+ cfprintf(cerr, "${blu}-mount${rs} ");
+ }
+ if (cmdline->mindepth != 0) {
+ cfprintf(cerr, "${blu}-mindepth${rs} ${bld}%d${rs} ", cmdline->mindepth);
+ }
+ if (cmdline->maxdepth != INT_MAX) {
+ cfprintf(cerr, "${blu}-maxdepth${rs} ${bld}%d${rs} ", cmdline->maxdepth);
+ }
+ if (cmdline->unique) {
+ cfprintf(cerr, "${blu}-unique${rs} ");
+ }
+
+ dump_expr(cerr, cmdline->expr, verbose);
+
+ fputs("\n", stderr);
+}
+
+/**
+ * Dump the estimated costs.
+ */
+static void dump_costs(const struct cmdline *cmdline) {
+ CFILE *cerr = cmdline->cerr;
+ const struct expr *expr = cmdline->expr;
+ cfprintf(cerr, " Cost: ~${ylw}%g${rs}\n", expr->cost);
+ cfprintf(cerr, "Probability: ~${ylw}%g%%${rs}\n", 100.0*expr->probability);
+}
+
+/**
+ * Get the current time.
+ */
+static int parse_gettime(struct timespec *ts) {
+#if _POSIX_TIMERS > 0
+ int ret = clock_gettime(CLOCK_REALTIME, ts);
+ if (ret != 0) {
+ perror("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 {
+ perror("gettimeofday()");
+ }
+ return ret;
+#endif
+}
+
+/**
+ * Parse the command line.
+ */
+struct cmdline *parse_cmdline(int argc, char *argv[]) {
+ struct cmdline *cmdline = malloc(sizeof(struct cmdline));
+ if (!cmdline) {
+ perror("malloc()");
+ goto fail;
+ }
+
+ cmdline->argv = NULL;
+ cmdline->paths = NULL;
+ cmdline->npaths = 0;
+ cmdline->colors = NULL;
+ cmdline->cout = NULL;
+ cmdline->cerr = NULL;
+ cmdline->mtab = NULL;
+ cmdline->mtab_error = 0;
+ cmdline->mindepth = 0;
+ cmdline->maxdepth = INT_MAX;
+ cmdline->flags = BFTW_RECOVER;
+ cmdline->strategy = BFTW_BFS;
+ cmdline->optlevel = 3;
+ cmdline->debug = 0;
+ cmdline->xargs_safe = false;
+ cmdline->ignore_races = false;
+ cmdline->unique = false;
+ cmdline->expr = &expr_true;
+ cmdline->nopen_files = 0;
+
+ trie_init(&cmdline->open_files);
+
+ static char* default_argv[] = {"bfs", NULL};
+ if (argc < 1) {
+ argc = 1;
+ argv = default_argv;
+ }
+
+ cmdline->argv = malloc((argc + 1)*sizeof(*cmdline->argv));
+ if (!cmdline->argv) {
+ perror("malloc()");
+ goto fail;
+ }
+ for (int i = 0; i <= argc; ++i) {
+ cmdline->argv[i] = argv[i];
+ }
+
+ enum use_color use_color = COLOR_AUTO;
+ if (getenv("NO_COLOR")) {
+ // https://no-color.org/
+ use_color = COLOR_NEVER;
+ }
+
+ cmdline->colors = parse_colors(getenv("LS_COLORS"));
+ cmdline->cout = cfdup(stdout, use_color ? cmdline->colors : NULL);
+ cmdline->cerr = cfdup(stderr, use_color ? cmdline->colors : NULL);
+ if (!cmdline->cout || !cmdline->cerr) {
+ perror("cfdup()");
+ goto fail;
+ }
+
+ cmdline->mtab = parse_bfs_mtab();
+ if (!cmdline->mtab) {
+ cmdline->mtab_error = errno;
+ }
+
+ bool stdin_tty = isatty(STDIN_FILENO);
+ bool stdout_tty = isatty(STDOUT_FILENO);
+ bool stderr_tty = isatty(STDERR_FILENO);
+
+ struct parser_state state = {
+ .cmdline = cmdline,
+ .argv = cmdline->argv + 1,
+ .command = cmdline->argv[0],
+ .regex_flags = 0,
+ .stdout_tty = stdout_tty,
+ .interactive = stdin_tty && stderr_tty,
+ .use_color = use_color,
+ .implicit_print = true,
+ .warn = stdin_tty,
+ .non_option_seen = false,
+ .just_info = false,
+ .last_arg = NULL,
+ .depth_arg = NULL,
+ .prune_arg = NULL,
+ };
+
+ if (strcmp(xbasename(state.command), "find") == 0) {
+ // Operate depth-first when invoked as "find"
+ cmdline->strategy = BFTW_DFS;
+ }
+
+ if (parse_gettime(&state.now) != 0) {
+ goto fail;
+ }
+
+ cmdline->expr = parse_whole_expr(&state);
+ if (!cmdline->expr) {
+ if (state.just_info) {
+ goto done;
+ } else {
+ goto fail;
+ }
+ }
+
+ if (optimize_cmdline(cmdline) != 0) {
+ goto fail;
+ }
+
+ if (cmdline->npaths == 0) {
+ if (parse_root(&state, ".") != 0) {
+ goto fail;
+ }
+ }
+
+ if ((cmdline->flags & BFTW_LOGICAL) && !cmdline->unique) {
+ // We need bftw() to detect cycles unless -unique does it for us
+ cmdline->flags |= BFTW_DETECT_CYCLES;
+ }
+
+ if (cmdline->debug & DEBUG_TREE) {
+ dump_cmdline(cmdline, false);
+ }
+ if (cmdline->debug & DEBUG_COST) {
+ dump_costs(cmdline);
+ }
+
+done:
+ return cmdline;
+
+fail:
+ free_cmdline(cmdline);
+ return NULL;
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2017-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 <assert.h>
+#include <errno.h>
+#include <grp.h>
+#include <pwd.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+typedef int bfs_printf_fn(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf);
+
+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;
+ /** The current mount table. */
+ const struct bfs_mtab *mtab;
+ /** The next printf directive in the chain. */
+ struct bfs_printf *next;
+};
+
+/** Print some text as-is. */
+static int bfs_printf_literal(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ size_t len = dstrlen(directive->str);
+ if (fwrite(directive->str, 1, len, file) == len) {
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+/** \c: flush */
+static int bfs_printf_flush(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ return fflush(file);
+}
+
+/**
+ * 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 && ret < sizeof(buf)); \
+ (void)ret
+
+/** %a, %c, %t: ctime() */
+static int bfs_printf_ctime(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %A, %B/%W, %C, %T: strftime() */
+static int bfs_printf_strftime(FILE *file, 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 && ret < sizeof(buf));
+ (void)ret;
+
+ return fprintf(file, directive->str, buf);
+}
+
+/** %b: blocks */
+static int bfs_printf_b(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %d: depth */
+static int bfs_printf_d(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ return fprintf(file, directive->str, (intmax_t)ftwbuf->depth);
+}
+
+/** %D: device */
+static int bfs_printf_D(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %f: file name */
+static int bfs_printf_f(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ return fprintf(file, directive->str, ftwbuf->path + ftwbuf->nameoff);
+}
+
+/** %F: file system type */
+static int bfs_printf_F(FILE *file, 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->mtab, statbuf);
+ return fprintf(file, directive->str, type);
+}
+
+/** %G: gid */
+static int bfs_printf_G(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %g: group name */
+static int bfs_printf_g(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ struct group *grp = getgrgid(statbuf->gid);
+ if (!grp) {
+ return bfs_printf_G(file, directive, ftwbuf);
+ }
+
+ return fprintf(file, directive->str, grp->gr_name);
+}
+
+/** %h: leading directories */
+static int bfs_printf_h(FILE *file, 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 = fprintf(file, directive->str, buf);
+ free(copy);
+ return ret;
+}
+
+/** %H: current root */
+static int bfs_printf_H(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ return fprintf(file, directive->str, ftwbuf->root);
+}
+
+/** %i: inode */
+static int bfs_printf_i(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %k: 1K blocks */
+static int bfs_printf_k(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %l: link target */
+static int bfs_printf_l(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ if (ftwbuf->typeflag != BFTW_LNK) {
+ return 0;
+ }
+
+ char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, 0);
+ if (!target) {
+ return -1;
+ }
+
+ int ret = fprintf(file, directive->str, target);
+ free(target);
+ return ret;
+}
+
+/** %m: mode */
+static int bfs_printf_m(FILE *file, 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(file, directive->str, (unsigned int)(statbuf->mode & 07777));
+}
+
+/** %M: symbolic mode */
+static int bfs_printf_M(FILE *file, 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];
+ format_mode(statbuf->mode, buf);
+ return fprintf(file, directive->str, buf);
+}
+
+/** %n: link count */
+static int bfs_printf_n(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %p: full path */
+static int bfs_printf_p(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ return fprintf(file, directive->str, ftwbuf->path);
+}
+
+/** %P: path after root */
+static int bfs_printf_P(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ const char *path = ftwbuf->path + strlen(ftwbuf->root);
+ if (path[0] == '/') {
+ ++path;
+ }
+ return fprintf(file, directive->str, path);
+}
+
+/** %s: size */
+static int bfs_printf_s(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %S: sparseness */
+static int bfs_printf_S(FILE *file, 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(file, directive->str, sparsity);
+}
+
+/** %U: uid */
+static int bfs_printf_U(FILE *file, 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(file, directive->str, buf);
+}
+
+/** %u: user name */
+static int bfs_printf_u(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ struct passwd *pwd = getpwuid(statbuf->uid);
+ if (!pwd) {
+ return bfs_printf_U(file, directive, ftwbuf);
+ }
+
+ return fprintf(file, directive->str, pwd->pw_name);
+}
+
+static const char *bfs_printf_type(enum bftw_typeflag typeflag) {
+ switch (typeflag) {
+ case BFTW_BLK:
+ return "b";
+ case BFTW_CHR:
+ return "c";
+ case BFTW_DIR:
+ return "d";
+ case BFTW_DOOR:
+ return "D";
+ case BFTW_FIFO:
+ return "p";
+ case BFTW_LNK:
+ return "l";
+ case BFTW_REG:
+ return "f";
+ case BFTW_SOCK:
+ return "s";
+ default:
+ return "U";
+ }
+}
+
+/** %y: type */
+static int bfs_printf_y(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ const char *type = bfs_printf_type(ftwbuf->typeflag);
+ return fprintf(file, directive->str, type);
+}
+
+/** %Y: target type */
+static int bfs_printf_Y(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+ int error = 0;
+
+ if (ftwbuf->typeflag != BFTW_LNK) {
+ return bfs_printf_y(file, directive, ftwbuf);
+ }
+
+ const char *type = "U";
+
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_FOLLOW);
+ if (statbuf) {
+ type = bfs_printf_type(bftw_mode_typeflag(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(file, directive->str, type);
+ if (error != 0) {
+ ret = -1;
+ errno = error;
+ }
+ return ret;
+}
+
+/**
+ * Free a printf directive.
+ */
+static void free_directive(struct bfs_printf *directive) {
+ if (directive) {
+ dstrfree(directive->str);
+ free(directive);
+ }
+}
+
+/**
+ * Create a new printf directive.
+ */
+static struct bfs_printf *new_directive(bfs_printf_fn *fn) {
+ struct bfs_printf *directive = malloc(sizeof(*directive));
+ if (!directive) {
+ perror("malloc()");
+ goto error;
+ }
+
+ directive->fn = fn;
+ directive->str = dstralloc(2);
+ if (!directive->str) {
+ perror("dstralloc()");
+ goto error;
+ }
+ directive->stat_field = 0;
+ directive->c = 0;
+ directive->mtab = NULL;
+ directive->next = NULL;
+ return directive;
+
+error:
+ free_directive(directive);
+ return NULL;
+}
+
+/**
+ * Append a printf directive to the chain.
+ */
+static struct bfs_printf **append_directive(struct bfs_printf **tail, struct bfs_printf *directive) {
+ assert(directive);
+ *tail = directive;
+ return &directive->next;
+}
+
+/**
+ * Append a literal string to the chain.
+ */
+static struct bfs_printf **append_literal(struct bfs_printf **tail, struct bfs_printf **literal) {
+ struct bfs_printf *directive = *literal;
+ if (directive && dstrlen(directive->str) > 0) {
+ *literal = NULL;
+ return append_directive(tail, directive);
+ } else {
+ return tail;
+ }
+}
+
+struct bfs_printf *parse_bfs_printf(const char *format, struct cmdline *cmdline) {
+ struct bfs_printf *head = NULL;
+ struct bfs_printf **tail = &head;
+
+ struct bfs_printf *literal = new_directive(bfs_printf_literal);
+ if (!literal) {
+ 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':
+ tail = append_literal(tail, &literal);
+ struct bfs_printf *directive = new_directive(bfs_printf_flush);
+ if (!directive) {
+ goto error;
+ }
+ tail = append_directive(tail, directive);
+ goto done;
+
+ case '\0':
+ bfs_error(cmdline, "'%s': Incomplete escape sequence '\\'.\n", format);
+ goto error;
+
+ default:
+ bfs_error(cmdline, "'%s': Unrecognized escape sequence '\\%c'.\n", format, c);
+ goto error;
+ }
+ } else if (c == '%') {
+ if (i[1] == '%') {
+ c = *++i;
+ goto one_char;
+ }
+
+ struct bfs_printf *directive = new_directive(NULL);
+ if (!directive) {
+ goto directive_error;
+ }
+ if (dstrapp(&directive->str, c) != 0) {
+ perror("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;
+ // Fallthrough
+ case ' ':
+ case '-':
+ if (strchr(directive->str, c)) {
+ bfs_error(cmdline, "'%s': Duplicate flag '%c'.\n", format, c);
+ goto directive_error;
+ }
+ if (dstrapp(&directive->str, c) != 0) {
+ perror("dstrapp()");
+ goto directive_error;
+ }
+ continue;
+ }
+
+ break;
+ }
+
+ // Parse the field width
+ while (c >= '0' && c <= '9') {
+ if (dstrapp(&directive->str, c) != 0) {
+ perror("dstrapp()");
+ goto directive_error;
+ }
+ c = *++i;
+ }
+
+ // Parse the precision
+ if (c == '.') {
+ do {
+ if (dstrapp(&directive->str, c) != 0) {
+ perror("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':
+ if (!cmdline->mtab) {
+ bfs_error(cmdline, "Couldn't parse the mount table: %s.\n", strerror(cmdline->mtab_error));
+ goto directive_error;
+ }
+ directive->fn = bfs_printf_F;
+ directive->mtab = cmdline->mtab;
+ break;
+ case 'g':
+ 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->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_error(cmdline, "'%s': Incomplete time specifier '%s%c'.\n", format, directive->str, i[-1]);
+ goto directive_error;
+ } else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) {
+ directive->c = c;
+ } else {
+ bfs_error(cmdline, "'%s': Unrecognized time specifier '%%%c%c'.\n", format, i[-1], c);
+ goto directive_error;
+ }
+ break;
+
+ case '\0':
+ bfs_error(cmdline, "'%s': Incomplete format specifier '%s'.\n", format, directive->str);
+ goto directive_error;
+
+ default:
+ bfs_error(cmdline, "'%s': Unrecognized format specifier '%%%c'.\n", format, c);
+ goto directive_error;
+ }
+
+ if (must_be_numeric && strcmp(specifier, "s") == 0) {
+ bfs_error(cmdline, "'%s': Invalid flags '%s' for string format '%%%c'.\n", format, directive->str + 1, c);
+ goto directive_error;
+ }
+
+ if (dstrcat(&directive->str, specifier) != 0) {
+ perror("dstrcat()");
+ goto directive_error;
+ }
+
+ tail = append_literal(tail, &literal);
+ tail = append_directive(tail, directive);
+
+ if (!literal) {
+ literal = new_directive(bfs_printf_literal);
+ if (!literal) {
+ goto error;
+ }
+ }
+
+ continue;
+
+ directive_error:
+ free_directive(directive);
+ goto error;
+ }
+
+ one_char:
+ if (dstrapp(&literal->str, c) != 0) {
+ perror("dstrapp()");
+ goto error;
+ }
+ }
+
+done:
+ tail = append_literal(tail, &literal);
+ if (head) {
+ free_directive(literal);
+ return head;
+ } else {
+ return literal;
+ }
+
+error:
+ free_directive(literal);
+ free_bfs_printf(head);
+ return NULL;
+}
+
+int bfs_printf(FILE *file, const struct bfs_printf *command, const struct BFTW *ftwbuf) {
+ int ret = 0, error = 0;
+
+ for (const struct bfs_printf *directive = command; directive; directive = directive->next) {
+ if (directive->fn(file, directive, ftwbuf) < 0) {
+ ret = -1;
+ error = errno;
+ }
+ }
+
+ errno = error;
+ return ret;
+}
+
+void free_bfs_printf(struct bfs_printf *command) {
+ while (command) {
+ struct bfs_printf *next = command->next;
+ free_directive(command);
+ command = next;
+ }
+}
+/****************************************************************************
+ * 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 <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.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,
+};
+
+/**
+ * A spawn action.
+ */
+struct bfs_spawn_action {
+ struct bfs_spawn_action *next;
+
+ enum bfs_spawn_op op;
+ int in_fd;
+ int out_fd;
+};
+
+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;
+ }
+}
+
+/** Facade for execvpe() which is non-standard. */
+static int bfs_execvpe(const char *exe, char **argv, char **envp) {
+#if __GLIBC__ || __linux__ || __NetBSD__ || __OpenBSD__
+ return execvpe(exe, argv, envp);
+#else
+ extern char **environ;
+ environ = envp;
+ return execvp(exe, argv);
+#endif
+}
+
+/** 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;
+ enum bfs_spawn_flags flags = ctx ? ctx->flags : 0;
+ const struct bfs_spawn_action *actions = ctx ? ctx->actions : NULL;
+
+ close(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->in_fd == pipefd[1] || action->out_fd == pipefd[1]) {
+ int fd = dup(pipefd[1]);
+ if (fd < 0) {
+ goto fail;
+ }
+ close(pipefd[1]);
+ pipefd[1] = fd;
+ }
+
+ 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;
+ }
+ }
+
+ if (flags & BFS_SPAWN_USEPATH) {
+ bfs_execvpe(exe, argv, envp);
+ } else {
+ execve(exe, argv, envp);
+ }
+
+fail:
+ error = errno;
+ while (write(pipefd[1], &error, sizeof(error)) < sizeof(error));
+ close(pipefd[1]);
+ _Exit(127);
+}
+
+pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ // Use a pipe to report errors from the child
+ int pipefd[2];
+ if (pipe_cloexec(pipefd) != 0) {
+ return -1;
+ }
+
+ int error;
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ error = errno;
+ close(pipefd[1]);
+ close(pipefd[0]);
+ errno = error;
+ return -1;
+ } else if (pid == 0) {
+ // Child
+ bfs_spawn_exec(exe, ctx, argv, envp, pipefd);
+ }
+
+ // Parent
+ close(pipefd[1]);
+
+ ssize_t nbytes = read(pipefd[0], &error, sizeof(error));
+ close(pipefd[0]);
+ if (nbytes == sizeof(error)) {
+ int wstatus;
+ waitpid(pid, &wstatus, 0);
+ errno = error;
+ return -1;
+ }
+
+ return pid;
+}
+/****************************************************************************
+ * 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 <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifdef STATX_BASIC_STATS
+# define HAVE_STATX true
+#elif __linux__
+# include <linux/stat.h>
+# include <sys/syscall.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_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(false);
+ 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_flag 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;
+
+ 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_flag 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) {
+ // -fsanitize=memory doesn't know about statx(), so tell it the memory
+ // got initialized
+#if BFS_HAS_FEATURE(memory_sanitizer, false)
+ 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_flag 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;
+
+ 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_flag 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);
+ if (ret != 0 && errno == ENOSYS) {
+ 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_flag flags, struct bfs_stat *buf) {
+ int at_flags = 0;
+ if (flags & BFS_STAT_NOFOLLOW) {
+ at_flags |= AT_SYMLINK_NOFOLLOW;
+ }
+
+ if (at_path) {
+ return bfs_stat_explicit(at_fd, at_path, at_flags, flags, buf);
+ }
+
+#ifdef AT_EMPTY_PATH
+ 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(false);
+ 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));
+}
+/****************************************************************************
+ * 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 <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(sizeof(*leaf) + 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 sizeof(struct trie_node) + size*sizeof(uintptr_t);
+}
+
+/** 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);
+ }
+}
+/****************************************************************************
+ * 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 <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];
+}
+/****************************************************************************
+ * bfs *
+ * Copyright (C) 2016-2018 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 <errno.h>
+#include <fcntl.h>
+#include <langinfo.h>
+#include <regex.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.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
+
+int xreaddir(DIR *dir, struct dirent **de) {
+ while (true) {
+ errno = 0;
+ *de = readdir(dir);
+
+ if (*de) {
+ const char *name = (*de)->d_name;
+ if (name[0] != '.' || (name[1] != '\0' && (name[1] != '.' || name[2] != '\0'))) {
+ return 0;
+ }
+ } else if (errno != 0) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+}
+
+char *xreadlinkat(int fd, const char *path, size_t size) {
+ ++size; // NUL-terminator
+ ssize_t len;
+ char *name = NULL;
+
+ 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 (len >= size) {
+ size *= 2;
+ } else {
+ break;
+ }
+ }
+
+ name[len] = '\0';
+ return name;
+
+error:
+ free(name);
+ return NULL;
+}
+
+bool isopen(int fd) {
+ return fcntl(fd, F_GETFD) >= 0 || errno != EBADF;
+}
+
+int redirect(int fd, const char *path, int flags, ...) {
+ mode_t mode = 0;
+ if (flags & O_CREAT) {
+ va_list args;
+ va_start(args, flags);
+
+ // Use int rather than mode_t, because va_arg must receive a
+ // fully-promoted type
+ mode = va_arg(args, int);
+
+ va_end(args);
+ }
+
+ int ret = open(path, flags, mode);
+
+ if (ret >= 0 && ret != fd) {
+ int orig = ret;
+ ret = dup2(orig, fd);
+ close(orig);
+ }
+
+ return ret;
+}
+
+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(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) {
+ int error = errno;
+ close(pipefd[1]);
+ close(pipefd[0]);
+ errno = error;
+ return -1;
+ }
+
+ return 0;
+#endif
+}
+
+char *xregerror(int err, const regex_t *regex) {
+ size_t len = regerror(err, regex, NULL, 0);
+ char *str = malloc(len);
+ if (str) {
+ regerror(err, regex, str, len);
+ }
+ return str;
+}
+
+int xlocaltime(const time_t *timep, struct tm *result) {
+ // Should be called before localtime_r() according to POSIX.1-2004
+ tzset();
+
+ if (localtime_r(timep, result)) {
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+void format_mode(mode_t mode, char str[11]) {
+ strcpy(str, "----------");
+
+ switch (bftw_mode_typeflag(mode)) {
+ case BFTW_BLK:
+ str[0] = 'b';
+ break;
+ case BFTW_CHR:
+ str[0] = 'c';
+ break;
+ case BFTW_DIR:
+ str[0] = 'd';
+ break;
+ case BFTW_DOOR:
+ str[0] = 'D';
+ break;
+ case BFTW_FIFO:
+ str[0] = 'p';
+ break;
+ case BFTW_LNK:
+ str[0] = 'l';
+ break;
+ case BFTW_SOCK:
+ str[0] = 's';
+ break;
+ default:
+ break;
+ }
+
+ 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;
+}
+
+bool is_nonexistence_error(int error) {
+ return error == ENOENT || errno == ENOTDIR;
+}
+
+/** Read a line from standard input. */
+static char *xgetline(void) {
+ char *line = dstralloc(0);
+ if (!line) {
+ return NULL;
+ }
+
+ while (true) {
+ int c = getchar();
+ if (c == '\n' || c == EOF) {
+ break;
+ }
+
+ if (dstrapp(&line, c) != 0) {
+ goto error;
+ }
+ }
+
+ return line;
+
+error:
+ dstrfree(line);
+ return NULL;
+}
+
+/** 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 REG_BADPAT;
+ }
+
+ regex_t regex;
+ int ret = regcomp(&regex, pattern, REG_EXTENDED);
+ if (ret != 0) {
+ return ret;
+ }
+
+ ret = regexec(&regex, response, 0, NULL, 0);
+ 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 != REG_NOMATCH) {
+ return -1;
+ }
+
+ ret = xrpregex(YESEXPR, response);
+ if (ret == 0) {
+ return 1;
+ } else if (ret != REG_NOMATCH) {
+ 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() {
+ fflush(stderr);
+
+ char *line = xgetline();
+ int ret = line ? xrpmatch(line) : -1;
+ dstrfree(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
+}
diff --git a/bfs.h b/bfs.h
deleted file mode 100644
index 41ac208..0000000
--- a/bfs.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-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. *
- ****************************************************************************/
-
-/**
- * Constants about the bfs program itself.
- */
-
-#ifndef BFS_H
-#define BFS_H
-
-#ifndef BFS_VERSION
-# define BFS_VERSION "1.5"
-#endif
-
-#ifndef BFS_HOMEPAGE
-# define BFS_HOMEPAGE "https://github.com/tavianator/bfs"
-#endif
-
-#endif // BFS_H
diff --git a/bftw.c b/bftw.c
deleted file mode 100644
index 510f7f3..0000000
--- a/bftw.c
+++ /dev/null
@@ -1,1551 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-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. *
- ****************************************************************************/
-
-/**
- * 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: Holds bftw_file's with open file descriptors, used for
- * openat() to minimize the amount of path re-traversals that need to happen.
- * Currently implemented as a priority queue based on depth and reference
- * count.
- *
- * - struct bftw_queue: The queue of bftw_file's left to explore. Implemented
- * as a simple circular buffer.
- *
- * - struct bftw_reader: A reader object that simplifies reading directories and
- * reporting errors.
- *
- * - struct bftw_state: Represents the current state of the traversal, allowing
- * various helper functions to take fewer parameters.
- */
-
-#include "bftw.h"
-#include "dstring.h"
-#include "stat.h"
-#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <dirent.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-/**
- * A file.
- */
-struct bftw_file {
- /** The parent directory, if any. */
- struct bftw_file *parent;
- /** This file's depth in the walk. */
- size_t depth;
- /** The root path this file was found from. */
- const char *root;
-
- /** The next file in the queue, if any. */
- struct bftw_file *next;
-
- /** Reference count. */
- size_t refcount;
- /** Index in the bftw_cache priority queue. */
- size_t heap_index;
-
- /** An open descriptor to this file, or -1. */
- int fd;
-
- /** This file's type, if known. */
- enum bftw_typeflag typeflag;
- /** 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 {
- /** A min-heap of open directories. */
- struct bftw_file **heap;
- /** Current heap size. */
- size_t size;
- /** Maximum heap size. */
- size_t capacity;
-};
-
-/** Initialize a cache. */
-static int bftw_cache_init(struct bftw_cache *cache, size_t capacity) {
- cache->heap = malloc(capacity*sizeof(*cache->heap));
- if (!cache->heap) {
- return -1;
- }
-
- cache->size = 0;
- cache->capacity = capacity;
- return 0;
-}
-
-/** Destroy a cache. */
-static void bftw_cache_destroy(struct bftw_cache *cache) {
- assert(cache->size == 0);
- free(cache->heap);
-}
-
-/** Check if two heap entries are in heap order. */
-static bool bftw_heap_check(const struct bftw_file *parent, const struct bftw_file *child) {
- if (parent->depth > child->depth) {
- return true;
- } else if (parent->depth < child->depth) {
- return false;
- } else {
- return parent->refcount <= child->refcount;
- }
-}
-
-/** Move a bftw_file to a particular place in the heap. */
-static void bftw_heap_move(struct bftw_cache *cache, struct bftw_file *file, size_t i) {
- cache->heap[i] = file;
- file->heap_index = i;
-}
-
-/** Bubble an entry up the heap. */
-static void bftw_heap_bubble_up(struct bftw_cache *cache, struct bftw_file *file) {
- size_t i = file->heap_index;
-
- while (i > 0) {
- size_t pi = (i - 1)/2;
- struct bftw_file *parent = cache->heap[pi];
- if (bftw_heap_check(parent, file)) {
- break;
- }
-
- bftw_heap_move(cache, parent, i);
- i = pi;
- }
-
- bftw_heap_move(cache, file, i);
-}
-
-/** Bubble an entry down the heap. */
-static void bftw_heap_bubble_down(struct bftw_cache *cache, struct bftw_file *file) {
- size_t i = file->heap_index;
-
- while (true) {
- size_t ci = 2*i + 1;
- if (ci >= cache->size) {
- break;
- }
-
- struct bftw_file *child = cache->heap[ci];
-
- size_t ri = ci + 1;
- if (ri < cache->size) {
- struct bftw_file *right = cache->heap[ri];
- if (!bftw_heap_check(child, right)) {
- ci = ri;
- child = right;
- }
- }
-
- if (bftw_heap_check(file, child)) {
- break;
- }
-
- bftw_heap_move(cache, child, i);
- i = ci;
- }
-
- bftw_heap_move(cache, file, i);
-}
-
-/** Bubble an entry up or down the heap. */
-static void bftw_heap_bubble(struct bftw_cache *cache, struct bftw_file *file) {
- size_t i = file->heap_index;
-
- if (i > 0) {
- size_t pi = (i - 1)/2;
- struct bftw_file *parent = cache->heap[pi];
- if (!bftw_heap_check(parent, file)) {
- bftw_heap_bubble_up(cache, file);
- return;
- }
- }
-
- bftw_heap_bubble_down(cache, file);
-}
-
-/** Increment a bftw_file's reference count. */
-static size_t bftw_file_incref(struct bftw_cache *cache, struct bftw_file *file) {
- size_t ret = ++file->refcount;
- if (file->fd >= 0) {
- bftw_heap_bubble_down(cache, file);
- }
- return ret;
-}
-
-/** Decrement a bftw_file's reference count. */
-static size_t bftw_file_decref(struct bftw_cache *cache, struct bftw_file *file) {
- size_t ret = --file->refcount;
- if (file->fd >= 0) {
- bftw_heap_bubble_up(cache, file);
- }
- return ret;
-}
-
-/** Add a bftw_file to the cache. */
-static void bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) {
- assert(cache->size < cache->capacity);
- assert(file->fd >= 0);
-
- size_t size = cache->size++;
- file->heap_index = size;
- bftw_heap_bubble_up(cache, file);
-}
-
-/** Remove a bftw_file from the cache. */
-static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) {
- assert(cache->size > 0);
- assert(file->fd >= 0);
-
- size_t size = --cache->size;
- size_t i = file->heap_index;
- if (i != size) {
- struct bftw_file *end = cache->heap[size];
- end->heap_index = i;
- bftw_heap_bubble(cache, end);
- }
-}
-
-/** 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);
-
- close(file->fd);
- file->fd = -1;
-}
-
-/** Pop a directory from the cache. */
-static void bftw_cache_pop(struct bftw_cache *cache) {
- assert(cache->size > 0);
- bftw_file_close(cache, cache->heap[0]);
-}
-
-/**
- * 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) {
- int ret = -1;
- struct bftw_file *file = NULL;
-
- if (cache->size >= 1) {
- file = cache->heap[0];
- if (file == saved && cache->size >= 2) {
- file = cache->heap[1];
- }
- }
-
- if (file && file != saved) {
- bftw_file_close(cache, file);
- ret = 0;
- }
-
- cache->capacity = cache->size;
- return ret;
-}
-
-/** 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_cache *cache, struct bftw_file *parent, const char *name) {
- size_t namelen = strlen(name);
- size_t size = sizeof(struct bftw_file) + namelen + 1;
-
- struct bftw_file *file = malloc(size);
- if (!file) {
- return NULL;
- }
-
- file->parent = parent;
-
- if (parent) {
- file->depth = parent->depth + 1;
- file->root = parent->root;
- file->nameoff = bftw_child_nameoff(parent);
- bftw_file_incref(cache, parent);
- } else {
- file->depth = 0;
- file->root = name;
- file->nameoff = 0;
- }
-
- file->next = NULL;
-
- file->refcount = 1;
- file->fd = -1;
-
- file->typeflag = BFTW_UNKNOWN;
- file->dev = -1;
- file->ino = -1;
-
- file->namelen = namelen;
- memcpy(file->name, name, namelen + 1);
-
- return file;
-}
-
-/**
- * Get the appropriate (fd, path) pair for the *at() family of functions.
- *
- * @param file
- * The file being accessed.
- * @param[out] at_fd
- * Will hold the appropriate file descriptor to use.
- * @param[in,out] at_path
- * Will hold the appropriate path to use.
- * @return The closest open ancestor file.
- */
-static struct bftw_file *bftw_file_base(struct bftw_file *file, int *at_fd, const char **at_path) {
- struct bftw_file *base = file;
-
- do {
- base = base->parent;
- } while (base && base->fd < 0);
-
- if (base) {
- *at_fd = base->fd;
- *at_path += bftw_child_nameoff(base);
- }
-
- return base;
-}
-
-/**
- * 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, const struct bftw_file *base, int at_fd, const char *at_path) {
- assert(file->fd < 0);
-
- 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(base->fd, at_path, flags);
- }
- }
-
- if (fd >= 0) {
- if (cache->size == cache->capacity) {
- 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) {
- int at_fd = AT_FDCWD;
- const char *at_path = path;
- struct bftw_file *base = bftw_file_base(file, &at_fd, &at_path);
-
- int fd = bftw_file_openat(cache, file, base, at_fd, at_path);
- if (fd >= 0 || errno != ENAMETOOLONG) {
- return fd;
- }
-
- // Handle ENAMETOOLONG by manually traversing the path component-by-component
-
- // -1 to include the root, which has depth == 0
- size_t offset = base ? base->depth : -1;
- size_t levels = file->depth - offset;
- if (levels < 2) {
- return fd;
- }
-
- struct bftw_file **parents = malloc(levels * sizeof(*parents));
- if (!parents) {
- return fd;
- }
-
- struct bftw_file *parent = file;
- for (size_t i = levels; i-- > 0;) {
- parents[i] = parent;
- parent = parent->parent;
- }
-
- for (size_t i = 0; i < levels; ++i) {
- fd = bftw_file_openat(cache, parents[i], base, at_fd, parents[i]->name);
- if (fd < 0) {
- break;
- }
-
- base = parents[i];
- at_fd = fd;
- }
-
- free(parents);
- return fd;
-}
-
-/**
- * Open a DIR* for a bftw_file.
- *
- * @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 DIR *, or NULL on error.
- */
-static 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;
- }
-
- // Now we dup() the fd and pass it to fdopendir(). This way we can
- // close the DIR* as soon as we're done with it, reducing the memory
- // footprint significantly, while keeping the fd around for future
- // openat() calls.
-
- int dfd = dup_cloexec(fd);
-
- if (dfd < 0 && errno == EMFILE) {
- if (bftw_cache_shrink(cache, file) == 0) {
- dfd = dup_cloexec(fd);
- }
- }
-
- if (dfd < 0) {
- return NULL;
- }
-
- DIR *ret = fdopendir(dfd);
- if (!ret) {
- int error = errno;
- close(dfd);
- errno = error;
- }
-
- return ret;
-}
-
-/** 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 tail of the queue. */
- struct bftw_file *tail;
-};
-
-/** Initialize a bftw_queue. */
-static void bftw_queue_init(struct bftw_queue *queue) {
- queue->head = NULL;
- queue->tail = NULL;
-}
-
-/** Add a file to the tail of the bftw_queue. */
-static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) {
- assert(file->next == NULL);
-
- if (!queue->head) {
- queue->head = file;
- }
- if (queue->tail) {
- queue->tail->next = file;
- }
- queue->tail = file;
-}
-
-/** Prepend a queue to the head of another one. */
-static void bftw_queue_prepend(struct bftw_queue *head, struct bftw_queue *tail) {
- if (head->tail) {
- head->tail->next = tail->head;
- }
- if (head->head) {
- tail->head = head->head;
- }
- if (!tail->tail) {
- tail->tail = head->tail;
- }
- head->head = NULL;
- head->tail = NULL;
-}
-
-/** 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;
- if (queue->tail == file) {
- queue->tail = NULL;
- }
- file->next = NULL;
- return file;
-}
-
-/**
- * A directory reader.
- */
-struct bftw_reader {
- /** The open handle to the directory. */
- DIR *dir;
- /** The current directory entry. */
- struct dirent *de;
- /** Any error code that has occurred. */
- int error;
-};
-
-/** Initialize a reader. */
-static void bftw_reader_init(struct bftw_reader *reader) {
- reader->dir = NULL;
- reader->de = NULL;
- reader->error = 0;
-}
-
-/** Open a directory for reading. */
-static int bftw_reader_open(struct bftw_reader *reader, struct bftw_cache *cache, struct bftw_file *file, const char *path) {
- assert(!reader->dir);
- assert(!reader->de);
-
- reader->error = 0;
-
- reader->dir = bftw_file_opendir(cache, file, path);
- if (!reader->dir) {
- reader->error = errno;
- return -1;
- }
-
- return 0;
-}
-
-/** Read a directory entry. */
-static int bftw_reader_read(struct bftw_reader *reader) {
- if (!reader->dir) {
- return -1;
- }
-
- if (xreaddir(reader->dir, &reader->de) != 0) {
- reader->error = errno;
- return -1;
- } else if (reader->de) {
- return 1;
- } else {
- return 0;
- }
-}
-
-/** Close a directory. */
-static int bftw_reader_close(struct bftw_reader *reader) {
- int ret = 0;
- if (reader->dir && closedir(reader->dir) != 0) {
- reader->error = errno;
- ret = -1;
- }
-
- reader->de = NULL;
- reader->dir = NULL;
- return ret;
-}
-
-/**
- * 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;
- /** An intermediate queue used for depth-first searches. */
- struct bftw_queue prequeue;
-
- /** The current path. */
- char *path;
- /** The current file. */
- struct bftw_file *file;
- /** The previous file. */
- struct bftw_file *previous;
- /** The reader for the current directory. */
- struct bftw_reader reader;
-
- /** 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 < 2) {
- errno = EMFILE;
- goto err;
- }
-
- // Reserve 1 fd for the open DIR *
- if (bftw_cache_init(&state->cache, args->nopenfd - 1) != 0) {
- goto err;
- }
-
- bftw_queue_init(&state->queue);
- bftw_queue_init(&state->prequeue);
-
- state->path = dstralloc(0);
- if (!state->path) {
- goto err_cache;
- }
-
- state->file = NULL;
- state->previous = NULL;
- bftw_reader_init(&state->reader);
-
- return 0;
-
-err_cache:
- bftw_cache_destroy(&state->cache);
-err:
- return -1;
-}
-
-enum bftw_typeflag bftw_mode_typeflag(mode_t mode) {
- switch (mode & S_IFMT) {
-#ifdef S_IFBLK
- case S_IFBLK:
- return BFTW_BLK;
-#endif
-#ifdef S_IFCHR
- case S_IFCHR:
- return BFTW_CHR;
-#endif
-#ifdef S_IFDIR
- case S_IFDIR:
- return BFTW_DIR;
-#endif
-#ifdef S_IFDOOR
- case S_IFDOOR:
- return BFTW_DOOR;
-#endif
-#ifdef S_IFIFO
- case S_IFIFO:
- return BFTW_FIFO;
-#endif
-#ifdef S_IFLNK
- case S_IFLNK:
- return BFTW_LNK;
-#endif
-#ifdef S_IFPORT
- case S_IFPORT:
- return BFTW_PORT;
-#endif
-#ifdef S_IFREG
- case S_IFREG:
- return BFTW_REG;
-#endif
-#ifdef S_IFSOCK
- case S_IFSOCK:
- return BFTW_SOCK;
-#endif
-#ifdef S_IFWHT
- case S_IFWHT:
- return BFTW_WHT;
-#endif
-
- default:
- return BFTW_UNKNOWN;
- }
-}
-
-static enum bftw_typeflag bftw_dirent_typeflag(const struct dirent *de) {
-#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN)
- switch (de->d_type) {
-#ifdef DT_BLK
- case DT_BLK:
- return BFTW_BLK;
-#endif
-#ifdef DT_CHR
- case DT_CHR:
- return BFTW_CHR;
-#endif
-#ifdef DT_DIR
- case DT_DIR:
- return BFTW_DIR;
-#endif
-#ifdef DT_DOOR
- case DT_DOOR:
- return BFTW_DOOR;
-#endif
-#ifdef DT_FIFO
- case DT_FIFO:
- return BFTW_FIFO;
-#endif
-#ifdef DT_LNK
- case DT_LNK:
- return BFTW_LNK;
-#endif
-#ifdef DT_PORT
- case DT_PORT:
- return BFTW_PORT;
-#endif
-#ifdef DT_REG
- case DT_REG:
- return BFTW_REG;
-#endif
-#ifdef DT_SOCK
- case DT_SOCK:
- return BFTW_SOCK;
-#endif
-#ifdef DT_WHT
- case DT_WHT:
- return BFTW_WHT;
-#endif
- }
-#endif
-
- return BFTW_UNKNOWN;
-}
-
-/** Cached bfs_stat(). */
-static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, struct bftw_stat *cache, enum bfs_stat_flag 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_flag 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;
-}
-
-enum bftw_typeflag bftw_typeflag(const struct BFTW *ftwbuf, enum bfs_stat_flag flags) {
- if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) {
- if ((flags & BFS_STAT_NOFOLLOW) || ftwbuf->typeflag != BFTW_LNK) {
- return ftwbuf->typeflag;
- }
- } else if ((flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW || ftwbuf->typeflag == BFTW_LNK) {
- return ftwbuf->typeflag;
- }
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags);
- if (statbuf) {
- return bftw_mode_typeflag(statbuf->mode);
- } else {
- return BFTW_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->typeflag == BFTW_UNKNOWN) {
- return true;
- }
-
- if (ftwbuf->typeflag == BFTW_LNK && !(ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- return true;
- }
-
- if (ftwbuf->typeflag == BFTW_DIR) {
- if (state->flags & (BFTW_DETECT_CYCLES | BFTW_XDEV)) {
- 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_maybe_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 bftw_reader *reader = &state->reader;
- const struct dirent *de = reader->de;
-
- struct BFTW *ftwbuf = &state->ftwbuf;
- ftwbuf->path = state->path;
- ftwbuf->root = file ? file->root : ftwbuf->path;
- ftwbuf->depth = 0;
- ftwbuf->visit = visit;
- ftwbuf->typeflag = BFTW_UNKNOWN;
- ftwbuf->error = reader->error;
- 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->typeflag = bftw_dirent_typeflag(de);
- ftwbuf->nameoff = bftw_child_nameoff(file);
- } else if (file) {
- parent = file->parent;
- ftwbuf->depth = file->depth;
- ftwbuf->typeflag = file->typeflag;
- 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->typeflag = BFTW_ERROR;
- return;
- }
-
- int follow_flags = BFTW_LOGICAL;
- if (ftwbuf->depth == 0) {
- follow_flags |= BFTW_COMFOLLOW;
- }
- 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->typeflag = bftw_mode_typeflag(statbuf->mode);
- } else {
- ftwbuf->typeflag = BFTW_ERROR;
- ftwbuf->error = errno;
- return;
- }
- }
-
- if (ftwbuf->typeflag == BFTW_DIR && (state->flags & BFTW_DETECT_CYCLES)) {
- for (const struct bftw_file *parent = file; parent; parent = parent->parent) {
- if (parent->depth == ftwbuf->depth) {
- continue;
- }
- if (parent->dev == statbuf->dev && parent->ino == statbuf->ino) {
- ftwbuf->typeflag = BFTW_ERROR;
- ftwbuf->error = ELOOP;
- return;
- }
- }
- }
-}
-
-/** 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 BFTW_ERROR unless BFTW_RECOVER is specified
- if (ftwbuf->typeflag == BFTW_ERROR && !(state->flags & BFTW_RECOVER)) {
- state->error = ftwbuf->error;
- return BFTW_STOP;
- }
-
- 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->typeflag != BFTW_DIR) {
- ret = BFTW_PRUNE;
- goto done;
- }
-
- if (state->flags & BFTW_XDEV) {
- const struct bftw_file *parent = state->file;
- if (parent && !name) {
- parent = parent->parent;
- }
-
- if (parent) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (statbuf && statbuf->dev != parent->dev) {
- 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(&state->cache, parent, name);
- if (!file) {
- state->error = errno;
- return -1;
- }
-
- struct dirent *de = state->reader.de;
- if (de) {
- file->typeflag = bftw_dirent_typeflag(de);
- }
-
- if (fill_id) {
- bftw_fill_id(file, &state->ftwbuf);
- }
-
- if (state->strategy == BFTW_DFS) {
- bftw_queue_push(&state->prequeue, file);
- } else {
- 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->strategy == BFTW_DFS) {
- bftw_queue_prepend(&state->prequeue, &state->queue);
- }
-
- if (!state->queue.head) {
- return 0;
- }
-
- state->file = bftw_queue_pop(&state->queue);
-
- if (bftw_build_path(state) != 0) {
- return -1;
- }
-
- return 1;
-}
-
-/**
- * Open a reader for the current directory.
- */
-static struct bftw_reader *bftw_open(struct bftw_state *state) {
- struct bftw_reader *reader = &state->reader;
- bftw_reader_open(reader, &state->cache, state->file, state->path);
- return reader;
-}
-
-/**
- * Close and release the reader.
- */
-static enum bftw_action bftw_release_reader(struct bftw_state *state, bool do_visit) {
- enum bftw_action ret = BFTW_CONTINUE;
-
- struct bftw_reader *reader = &state->reader;
- bftw_reader_close(reader);
-
- if (reader->error != 0) {
- if (do_visit) {
- if (bftw_visit(state, NULL, BFTW_PRE) == BFTW_STOP) {
- ret = BFTW_STOP;
- }
- } else {
- state->error = reader->error;
- }
- reader->error = 0;
- }
-
- return ret;
-}
-
-/**
- * Finalize and free a file we're done with.
- */
-static enum bftw_action bftw_release_file(struct bftw_state *state, bool visit_file, bool visit_parents) {
- enum bftw_action ret = BFTW_CONTINUE;
-
- if (!(state->flags & BFTW_DEPTH)) {
- visit_file = false;
- visit_parents = false;
- }
- bool do_visit = visit_file;
-
- while (state->file) {
- if (bftw_file_decref(&state->cache, state->file) > 0) {
- state->file = NULL;
- break;
- }
-
- if (do_visit) {
- if (bftw_visit(state, NULL, BFTW_POST) == BFTW_STOP) {
- ret = BFTW_STOP;
- visit_parents = false;
- }
- }
- do_visit = visit_parents;
-
- struct bftw_file *parent = state->file->parent;
- if (state->previous == state->file) {
- state->previous = parent;
- }
- bftw_file_free(&state->cache, state->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_release_file(state, false, false);
- }
-}
-
-/**
- * Dispose of the bftw() state.
- *
- * @return
- * The bftw() return value.
- */
-static int bftw_state_destroy(struct bftw_state *state) {
- dstrfree(state->path);
-
- bftw_release_reader(state, false);
-
- bftw_release_file(state, false, false);
- bftw_drain_queue(state, &state->prequeue);
- bftw_drain_queue(state, &state->queue);
-
- bftw_cache_destroy(&state->cache);
-
- errno = state->error;
- return state->error ? -1 : 0;
-}
-
-/**
- * Breadth-first bftw() implementation.
- */
-static int bftw_bfs(const struct bftw_args *args) {
- struct bftw_state state;
- if (bftw_state_init(&state, args) != 0) {
- return -1;
- }
-
- 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;
- }
- }
-
- while (bftw_pop(&state) > 0) {
- struct bftw_reader *reader = bftw_open(&state);
-
- while (bftw_reader_read(reader) > 0) {
- const char *name = reader->de->d_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;
- }
- }
-
- if (bftw_release_reader(&state, true) == BFTW_STOP) {
- goto done;
- }
- if (bftw_release_file(&state, true, true) == BFTW_STOP) {
- goto done;
- }
- }
-
-done:
- return bftw_state_destroy(&state);
-}
-
-/**
- * Depth-first bftw() implementation.
- */
-static int bftw_dfs(const struct bftw_args *args) {
- struct bftw_state state;
- if (bftw_state_init(&state, args) != 0) {
- return -1;
- }
-
- for (size_t i = 0; i < args->npaths; ++i) {
- if (bftw_push(&state, args->paths[i], false) != 0) {
- goto done;
- }
- }
-
- while (bftw_pop(&state) > 0) {
- bool visit_post = true;
-
- switch (bftw_visit(&state, NULL, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- visit_post = false;
- goto next;
- case BFTW_STOP:
- goto done;
- }
-
- struct bftw_reader *reader = bftw_open(&state);
-
- while (bftw_reader_read(reader) > 0) {
- if (bftw_push(&state, reader->de->d_name, false) != 0) {
- goto done;
- }
- }
-
- if (bftw_release_reader(&state, true) == BFTW_STOP) {
- goto done;
- }
-
- next:
- if (bftw_release_file(&state, visit_post, true) == BFTW_STOP) {
- goto done;
- }
- }
-
-done:
- return bftw_state_destroy(&state);
-}
-
-/**
- * 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;
- /** The current target depth. */
- size_t 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;
-
- struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
- mutbuf->visit = state->visit;
-
- if (ftwbuf->typeflag == BFTW_ERROR) {
- if (state->depth - ftwbuf->depth <= 1) {
- return state->delegate(ftwbuf, state->ptr);
- } else {
- return BFTW_PRUNE;
- }
- }
-
- if (ftwbuf->depth < state->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;
- }
- }
-
- state->bottom = false;
-
- enum bftw_action ret = state->delegate(ftwbuf, state->ptr);
-
- switch (ret) {
- case BFTW_CONTINUE:
- ret = BFTW_PRUNE;
- break;
- case BFTW_PRUNE:
- if (ftwbuf->typeflag == BFTW_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;
-}
-
-/**
- * Iterative deepening bftw() wrapper.
- */
-static int bftw_ids(const struct bftw_args *args) {
- struct trie pruned;
- trie_init(&pruned);
-
- struct bftw_ids_state state = {
- .delegate = args->callback,
- .ptr = args->ptr,
- .visit = BFTW_PRE,
- .depth = 0,
- .pruned = &pruned,
- .bottom = false,
- };
-
- struct bftw_args ids_args = *args;
- ids_args.callback = bftw_ids_callback;
- ids_args.ptr = &state;
- ids_args.flags &= ~BFTW_DEPTH;
- ids_args.strategy = BFTW_DFS;
-
- int ret = 0;
-
- while (ret == 0 && !state.quit && !state.bottom) {
- state.bottom = true;
-
- // bftw_bfs() is more efficient than bftw_dfs() since it visits
- // directory entries as it reads them. With args->strategy ==
- // BFTW_DFS, it gives a hybrid ordering that visits immediate
- // children first, then deeper descendants depth-first. This
- // doesn't matter for iterative deepening since we only visit
- // one level at a time.
- ret = bftw_bfs(&ids_args);
-
- ++state.depth;
- }
-
- if (args->flags & BFTW_DEPTH) {
- state.visit = BFTW_POST;
-
- while (ret == 0 && !state.quit && state.depth > 0) {
- --state.depth;
- ret = bftw_bfs(&ids_args);
- }
- }
-
- if (state.error) {
- ret = -1;
- } else {
- state.error = errno;
- }
- trie_destroy(&pruned);
- errno = state.error;
- return ret;
-}
-
-int bftw(const struct bftw_args *args) {
- switch (args->strategy) {
- case BFTW_BFS:
- return bftw_bfs(args);
- case BFTW_DFS:
- return bftw_dfs(args);
- case BFTW_IDS:
- return bftw_ids(args);
- }
-
- errno = EINVAL;
- return -1;
-}
diff --git a/bftw.h b/bftw.h
deleted file mode 100644
index 573c5a9..0000000
--- a/bftw.h
+++ /dev/null
@@ -1,238 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-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 file-walking API based on nftw().
- */
-
-#ifndef BFS_BFTW_H
-#define BFS_BFTW_H
-
-#include "mtab.h"
-#include "stat.h"
-#include <stddef.h>
-#include <sys/types.h>
-
-/**
- * Possible file types.
- */
-enum bftw_typeflag {
- /** Unknown type. */
- BFTW_UNKNOWN = 0,
- /** Block device. */
- BFTW_BLK = 1 << 0,
- /** Character device. */
- BFTW_CHR = 1 << 1,
- /** Directory. */
- BFTW_DIR = 1 << 2,
- /** Solaris door. */
- BFTW_DOOR = 1 << 3,
- /** Pipe. */
- BFTW_FIFO = 1 << 4,
- /** Symbolic link. */
- BFTW_LNK = 1 << 5,
- /** Solaris event port. */
- BFTW_PORT = 1 << 6,
- /** Regular file. */
- BFTW_REG = 1 << 7,
- /** Socket. */
- BFTW_SOCK = 1 << 8,
- /** BSD whiteout. */
- BFTW_WHT = 1 << 9,
- /** An error occurred for this file. */
- BFTW_ERROR = 1 << 10,
-};
-
-/**
- * Convert a bfs_stat() mode to a bftw() typeflag.
- */
-enum bftw_typeflag bftw_mode_typeflag(mode_t mode);
-
-/**
- * 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 bftw_typeflag typeflag;
- /** The errno that occurred, if typeflag == 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_flag 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_flag 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 bftw_typeflag bftw_typeflag(const struct BFTW *ftwbuf, enum bfs_stat_flag 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_DEPTH = 1 << 2,
- /** If the initial path is a symbolic link, follow it. */
- BFTW_COMFOLLOW = 1 << 3,
- /** Follow all symbolic links. */
- BFTW_LOGICAL = 1 << 4,
- /** Detect directory cycles. */
- BFTW_DETECT_CYCLES = 1 << 5,
- /** Stay on the same filesystem. */
- BFTW_XDEV = 1 << 6,
-};
-
-/**
- * Tree search strategies for bftw().
- */
-enum bftw_strategy {
- /** Breadth-first search. */
- BFTW_BFS,
- /** Depth-first search. */
- BFTW_DFS,
- /** Iterative deepening search. */
- BFTW_IDS,
-};
-
-/**
- * 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/cmdline.h b/cmdline.h
deleted file mode 100644
index 29c8d25..0000000
--- a/cmdline.h
+++ /dev/null
@@ -1,132 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2018 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. *
- ****************************************************************************/
-
-/**
- * Representation of the parsed command line.
- */
-
-#ifndef BFS_CMDLINE_H
-#define BFS_CMDLINE_H
-
-#include "color.h"
-#include "trie.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,
-};
-
-/**
- * The parsed command line.
- */
-struct cmdline {
- /** The unparsed command line arguments. */
- char **argv;
-
- /** The root paths. */
- const char **paths;
- /** The number of root paths. */
- size_t npaths;
-
- /** Color data. */
- struct colors *colors;
- /** Colored stdout. */
- CFILE *cout;
- /** Colored stderr. */
- CFILE *cerr;
-
- /** Table of mounted file systems. */
- struct bfs_mtab *mtab;
- /** The error that occurred parsing the mount table, if any. */
- int mtab_error;
-
- /** -mindepth option. */
- int mindepth;
- /** -maxdepth option. */
- int maxdepth;
-
- /** bftw() flags. */
- enum bftw_flags flags;
- /** bftw() search strategy. */
- enum bftw_strategy strategy;
-
- /** Optimization level. */
- int optlevel;
- /** Debugging flags. */
- enum debug_flags debug;
- /** Whether to only handle paths with xargs-safe characters. */
- bool xargs_safe;
- /** Whether to ignore deletions that race with bfs. */
- bool ignore_races;
- /** Whether to only return unique files. */
- bool unique;
-
- /** The command line expression. */
- struct expr *expr;
-
- /** All the open files owned by the command line. */
- struct trie open_files;
- /** The number of open files owned by the command line. */
- int nopen_files;
-};
-
-/**
- * Parse the command line.
- */
-struct cmdline *parse_cmdline(int argc, char *argv[]);
-
-/**
- * Dump the parsed command line.
- */
-void dump_cmdline(const struct cmdline *cmdline, bool verbose);
-
-/**
- * Optimize the parsed command line.
- *
- * @return 0 if successful, -1 on error.
- */
-int optimize_cmdline(struct cmdline *cmdline);
-
-/**
- * Evaluate the command line.
- */
-int eval_cmdline(const struct cmdline *cmdline);
-
-/**
- * Free the parsed command line.
- *
- * @return 0 if successful, -1 on error.
- */
-int free_cmdline(struct cmdline *cmdline);
-
-#endif // BFS_CMDLINE_H
diff --git a/color.c b/color.c
deleted file mode 100644
index c397050..0000000
--- a/color.c
+++ /dev/null
@@ -1,931 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-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 "color.h"
-#include "bftw.h"
-#include "dstring.h"
-#include "fsade.h"
-#include "stat.h"
-#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-/**
- * The parsed form of LS_COLORS.
- */
-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 void set_color(struct colors *colors, const char *name, char *value) {
- char **color = get_color(colors, name);
- if (color) {
- dstrfree(*color);
- *color = value;
- }
-}
-
-/**
- * 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;
-}
-
-struct colors *parse_colors(const char *ls_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", &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, "wr", "01;33", &colors->warning);
- ret |= init_color(colors, "er", "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;
- }
-
- 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;
- }
-
- set_color(colors, key, value);
- free(key);
- }
- }
-
- if (colors->link && strcmp(colors->link, "target") == 0) {
- colors->link_as_target = true;
- dstrfree(colors->link);
- colors->link = NULL;
- }
-
- 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 *cfopen(const char *path, const struct colors *colors) {
- CFILE *cfile = malloc(sizeof(*cfile));
- if (!cfile) {
- return NULL;
- }
-
- cfile->close = false;
- cfile->file = fopen(path, "wb");
- if (!cfile->file) {
- cfclose(cfile);
- return NULL;
- }
- cfile->close = true;
-
- if (isatty(fileno(cfile->file))) {
- cfile->colors = colors;
- } else {
- cfile->colors = NULL;
- }
-
- return cfile;
-}
-
-CFILE *cfdup(FILE *file, const struct colors *colors) {
- CFILE *cfile = malloc(sizeof(*cfile));
- if (!cfile) {
- return NULL;
- }
-
- cfile->close = false;
- cfile->file = file;
-
- if (isatty(fileno(file))) {
- cfile->colors = colors;
- } else {
- cfile->colors = NULL;
- }
-
- return cfile;
-}
-
-int cfclose(CFILE *cfile) {
- int ret = 0;
- if (cfile) {
- 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_flag flags) {
- enum bftw_typeflag typeflag = bftw_typeflag(ftwbuf, flags);
- if (typeflag == BFTW_ERROR) {
- goto error;
- }
-
- const struct bfs_stat *statbuf = NULL;
- const char *color = NULL;
-
- switch (typeflag) {
- case BFTW_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 BFTW_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 BFTW_LNK:
- if (colors->orphan && is_link_broken(ftwbuf)) {
- color = colors->orphan;
- } else {
- color = colors->link;
- }
- break;
-
- case BFTW_BLK:
- color = colors->blockdev;
- break;
- case BFTW_CHR:
- color = colors->chardev;
- break;
- case BFTW_FIFO:
- color = colors->pipe;
- break;
- case BFTW_SOCK:
- color = colors->socket;
- break;
- case BFTW_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 a fixed-length string. */
-static int print_strn(const char *str, size_t len, FILE *file) {
- if (fwrite(str, 1, len, file) == len) {
- return 0;
- } else {
- return -1;
- }
-}
-
-/** Print a dstring. */
-static int print_dstr(const char *str, FILE *file) {
- return print_strn(str, dstrlen(str), file);
-}
-
-/** Print an ANSI escape sequence. */
-static int print_esc(const struct colors *colors, const char *esc, FILE *file) {
- if (print_dstr(colors->leftcode, file) != 0) {
- return -1;
- }
- if (print_dstr(esc, file) != 0) {
- return -1;
- }
- if (print_dstr(colors->rightcode, file) != 0) {
- return -1;
- }
-
- return 0;
-}
-
-/** Reset after an ANSI escape sequence. */
-static int print_reset(const struct colors *colors, FILE *file) {
- if (colors->endcode) {
- return print_dstr(colors->endcode, file);
- } else {
- return print_esc(colors, colors->reset, file);
- }
-}
-
-/** Print a string with an optional color. */
-static int print_colored(const struct colors *colors, const char *esc, const char *str, size_t len, FILE *file) {
- if (esc) {
- if (print_esc(colors, esc, file) != 0) {
- return -1;
- }
- }
- if (print_strn(str, len, file) != 0) {
- return -1;
- }
- if (esc) {
- if (print_reset(colors, file) != 0) {
- return -1;
- }
- }
-
- return 0;
-}
-
-/** Print a path with colors. */
-static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flag flags) {
- const struct colors *colors = cfile->colors;
- FILE *file = cfile->file;
-
- size_t nameoff;
- if (path == ftwbuf->path) {
- nameoff = ftwbuf->nameoff;
- } else {
- nameoff = xbasename(path) - path;
- }
-
- if (nameoff > 0) {
- if (print_colored(colors, colors->directory, path, nameoff, file) != 0) {
- return -1;
- }
- }
-
- const char *filename = path + nameoff;
- const char *color = file_color(colors, filename, ftwbuf, flags);
- return print_colored(colors, color, filename, strlen(filename), file);
-}
-
-/** 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 fputs(ftwbuf->path, cfile->file) == EOF ? -1 : 0;
- }
-
- enum bfs_stat_flag flags = ftwbuf->stat_flags;
- if (colors && colors->link_as_target && ftwbuf->typeflag == BFTW_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) {
- int ret = -1;
-
- size_t len = 0;
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_NOFOLLOW);
- if (statbuf) {
- len = statbuf->size;
- }
-
- char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
- if (!target) {
- goto done;
- }
-
- if (!cfile->colors) {
- ret = fputs(target, cfile->file) == EOF ? -1 : 0;
- goto done;
- }
-
- ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW);
-
-done:
- free(target);
- 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;
-}
-
-int cvfprintf(CFILE *cfile, const char *format, va_list args) {
- const struct colors *colors = cfile->colors;
- FILE *file = cfile->file;
- int error = errno;
-
- for (const char *i = format; *i; ++i) {
- size_t verbatim = strcspn(i, "%$");
- if (fwrite(i, 1, verbatim, file) != verbatim) {
- return -1;
- }
-
- i += verbatim;
- switch (*i) {
- case '%':
- switch (*++i) {
- case '%':
- if (fputc('%', file) == EOF) {
- return -1;
- }
- break;
-
- case 'c':
- if (fputc(va_arg(args, int), file) == EOF) {
- return -1;
- }
- break;
-
- case 'd':
- if (fprintf(file, "%d", va_arg(args, int)) < 0) {
- return -1;
- }
- break;
-
- case 'g':
- if (fprintf(file, "%g", va_arg(args, double)) < 0) {
- return -1;
- }
- break;
-
- case 's':
- if (fputs(va_arg(args, const char *), file) == EOF) {
- return -1;
- }
- break;
-
- case 'z':
- ++i;
- if (*i != 'u') {
- goto invalid;
- }
- if (fprintf(file, "%zu", va_arg(args, size_t)) < 0) {
- return -1;
- }
- break;
-
- case 'm':
- if (fputs(strerror(error), file) == EOF) {
- return -1;
- }
- break;
-
- case 'p':
- switch (*++i) {
- 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;
-
- default:
- goto invalid;
- }
-
- break;
-
- default:
- goto invalid;
- }
- break;
-
- case '$':
- switch (*++i) {
- case '$':
- if (fputc('$', file) == EOF) {
- 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(colors, *esc, file) != 0) {
- return -1;
- }
- }
-
- i = end;
- break;
- }
-
- default:
- goto invalid;
- }
- break;
-
- default:
- return 0;
- }
-
- }
-
- return 0;
-
-invalid:
- assert(false);
- errno = EINVAL;
- return -1;
-}
diff --git a/color.h b/color.h
deleted file mode 100644
index 7df3785..0000000
--- a/color.h
+++ /dev/null
@@ -1,124 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2018 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 "bftw.h"
-#include "util.h"
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-
-/**
- * A lookup table for colors.
- */
-struct colors;
-
-/**
- * Parse a color table.
- *
- * @param ls_colors
- * A color table in the LS_COLORS environment variable format.
- * @return The parsed color table.
- */
-struct colors *parse_colors(const char *ls_colors);
-
-/**
- * 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;
- /** Whether to close the underlying stream. */
- bool close;
-} CFILE;
-
-/**
- * Open a file for colored output.
- *
- * @param path
- * The path to the file to open.
- * @param colors
- * The color table to use if file is a TTY.
- * @return A colored file stream.
- */
-CFILE *cfopen(const char *path, const struct colors *colors);
-
-/**
- * Make a colored copy of an open file.
- *
- * @param file
- * The underlying file.
- * @param colors
- * The color table to use if file is a TTY.
- * @return A colored wrapper around file.
- */
-CFILE *cfdup(FILE *file, const struct colors *colors);
-
-/**
- * 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)
- * %pP: A colored file path, from a const struct BFTW * argument
- * %pL: A colored link target, from a const struct BFTW * argument
- * %%: 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/diag.c b/diag.c
deleted file mode 100644
index bdbf98e..0000000
--- a/diag.c
+++ /dev/null
@@ -1,64 +0,0 @@
-/****************************************************************************
- * 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. *
- ****************************************************************************/
-
-#include "diag.h"
-#include "cmdline.h"
-#include "color.h"
-#include "util.h"
-#include <errno.h>
-#include <stdarg.h>
-#include <stdlib.h>
-#include <string.h>
-
-void bfs_error(const struct cmdline *cmdline, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bfs_verror(cmdline, format, args);
- va_end(args);
-}
-
-void bfs_warning(const struct cmdline *cmdline, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bfs_vwarning(cmdline, format, args);
- va_end(args);
-}
-
-void bfs_verror(const struct cmdline *cmdline, const char *format, va_list args) {
- int error = errno;
-
- bfs_error_prefix(cmdline);
-
- errno = error;
- cvfprintf(cmdline->cerr, format, args);
-}
-
-void bfs_vwarning(const struct cmdline *cmdline, const char *format, va_list args) {
- int error = errno;
-
- bfs_warning_prefix(cmdline);
-
- errno = error;
- cvfprintf(cmdline->cerr, format, args);
-}
-
-void bfs_error_prefix(const struct cmdline *cmdline) {
- cfprintf(cmdline->cerr, "${bld}%s:${rs} ${er}error:${rs} ", xbasename(cmdline->argv[0]));
-}
-
-void bfs_warning_prefix(const struct cmdline *cmdline) {
- cfprintf(cmdline->cerr, "${bld}%s:${rs} ${wr}warning:${rs} ", xbasename(cmdline->argv[0]));
-}
diff --git a/diag.h b/diag.h
deleted file mode 100644
index 54f2366..0000000
--- a/diag.h
+++ /dev/null
@@ -1,60 +0,0 @@
-/****************************************************************************
- * 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. *
- ****************************************************************************/
-
-/**
- * Formatters for diagnostic messages.
- */
-
-#ifndef BFS_DIAG_H
-#define BFS_DIAG_H
-
-#include "cmdline.h"
-#include "util.h"
-#include <stdarg.h>
-
-/**
- * Shorthand for printing error messages.
- */
-BFS_FORMATTER(2, 3)
-void bfs_error(const struct cmdline *cmdline, const char *format, ...);
-
-/**
- * Shorthand for printing warning messages.
- */
-BFS_FORMATTER(2, 3)
-void bfs_warning(const struct cmdline *cmdline, const char *format, ...);
-
-/**
- * bfs_error() variant that takes a va_list.
- */
-void bfs_verror(const struct cmdline *cmdline, const char *format, va_list args);
-
-/**
- * bfs_warning() variant that takes a va_list.
- */
-void bfs_vwarning(const struct cmdline *cmdline, const char *format, va_list args);
-
-/**
- * Print the error message prefix.
- */
-void bfs_error_prefix(const struct cmdline *cmdline);
-
-/**
- * Print the warning message prefix.
- */
-void bfs_warning_prefix(const struct cmdline *cmdline);
-
-#endif // BFS_DIAG_H
diff --git a/dstring.c b/dstring.c
deleted file mode 100644
index d7ce456..0000000
--- a/dstring.c
+++ /dev/null
@@ -1,152 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-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 "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 sizeof(struct dstring) + capacity + 1;
-}
-
-/** Allocate a dstring with the given contents. */
-static char *dstralloc_impl(size_t capacity, size_t length, const char *data) {
- struct dstring *header = malloc(dstrsize(capacity));
- if (!header) {
- return NULL;
- }
-
- header->capacity = capacity;
- header->length = length;
- return memcpy(header->data, data, length + 1);
-}
-
-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);
-}
-
-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 dstrapp(char **str, char c) {
- return dstrcat_impl(str, &c, 1);
-}
-
-char *dstrprintf(const char *format, ...) {
- va_list args;
-
- va_start(args, format);
- int len = vsnprintf(NULL, 0, format, args);
- va_end(args);
-
- assert(len > 0);
-
- char *str = dstralloc(len);
- if (!str) {
- return NULL;
- }
-
- va_start(args, format);
- len = vsnprintf(str, len + 1, format, args);
- va_end(args);
-
- struct dstring *header = dstrheader(str);
- assert(len == header->capacity);
- header->length = len;
-
- return str;
-}
-
-void dstrfree(char *dstr) {
- if (dstr) {
- free(dstrheader(dstr));
- }
-}
diff --git a/dstring.h b/dstring.h
deleted file mode 100644
index 22476b9..0000000
--- a/dstring.h
+++ /dev/null
@@ -1,130 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-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 dynamic string library.
- */
-
-#ifndef BFS_DSTRING_H
-#define BFS_DSTRING_H
-
-#include "util.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);
-
-/**
- * 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 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, ...);
-
-/**
- * Free a dynamic string.
- *
- * @param dstr
- * The string to free.
- */
-void dstrfree(char *dstr);
-
-#endif // BFS_DSTRING_H
diff --git a/eval.c b/eval.c
deleted file mode 100644
index d89e62a..0000000
--- a/eval.c
+++ /dev/null
@@ -1,1391 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-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. *
- ****************************************************************************/
-
-/**
- * Implementation of all the literal expressions.
- */
-
-#include "eval.h"
-#include "bftw.h"
-#include "cmdline.h"
-#include "color.h"
-#include "diag.h"
-#include "dstring.h"
-#include "exec.h"
-#include "fsade.h"
-#include "mtab.h"
-#include "printf.h"
-#include "stat.h"
-#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <dirent.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>
-
-struct eval_state {
- /** Data about the current file. */
- const struct BFTW *ftwbuf;
- /** The parsed command line. */
- const struct cmdline *cmdline;
- /** The bftw() callback return value. */
- enum bftw_action action;
- /** The eval_cmdline() return value. */
- int *ret;
- /** Whether to quit immediately. */
- bool quit;
-};
-
-/**
- * Print an error message.
- */
-BFS_FORMATTER(2, 3)
-static void eval_error(struct eval_state *state, const char *format, ...) {
- int error = errno;
- const struct cmdline *cmdline = state->cmdline;
- CFILE *cerr = cmdline->cerr;
-
- bfs_error(cmdline, "%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 eval_state *state, int error) {
- return state->cmdline->ignore_races
- && is_nonexistence_error(error)
- && state->ftwbuf->depth > 0;
-}
-
-/**
- * Report an error that occurs during evaluation.
- */
-static void eval_report_error(struct eval_state *state) {
- if (!eval_should_ignore(state, errno)) {
- eval_error(state, "%m.\n");
- *state->ret = EXIT_FAILURE;
- }
-}
-
-/**
- * Perform a bfs_stat() call if necessary.
- */
-static const struct bfs_stat *eval_stat(struct eval_state *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 expr_cmp(const struct expr *expr, long long n) {
- switch (expr->cmp_flag) {
- case CMP_EXACT:
- return n == expr->idata;
- case CMP_LESS:
- return n < expr->idata;
- case CMP_GREATER:
- return n > expr->idata;
- }
-
- return false;
-}
-
-/**
- * -true test.
- */
-bool eval_true(const struct expr *expr, struct eval_state *state) {
- return true;
-}
-
-/**
- * -false test.
- */
-bool eval_false(const struct expr *expr, struct eval_state *state) {
- return false;
-}
-
-/**
- * -executable, -readable, -writable tests.
- */
-bool eval_access(const struct expr *expr, struct eval_state *state) {
- const struct BFTW *ftwbuf = state->ftwbuf;
- return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, expr->idata) == 0;
-}
-
-/**
- * -acl test.
- */
-bool eval_acl(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *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 eval_state *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));
- *state->ret = EXIT_FAILURE;
- }
- return ret;
-}
-
-/**
- * -[aBcm]?newer tests.
- */
-bool eval_newer(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *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 MINUTES:
- diff /= 60;
- break;
- case DAYS:
- diff /= 60*60*24;
- break;
- }
-
- return expr_cmp(expr, diff);
-}
-
-/**
- * -used test.
- */
-bool eval_used(const struct expr *expr, struct eval_state *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;
- }
-
- time_t diff = timespec_diff(atime, ctime);
- diff /= 60*60*24;
- return expr_cmp(expr, diff);
-}
-
-/**
- * -gid test.
- */
-bool eval_gid(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- return expr_cmp(expr, statbuf->gid);
-}
-
-/**
- * -uid test.
- */
-bool eval_uid(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- return expr_cmp(expr, statbuf->uid);
-}
-
-/**
- * -nogroup test.
- */
-bool eval_nogroup(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- errno = 0;
- if (getgrgid(statbuf->gid) == NULL) {
- if (errno == 0) {
- return true;
- } else {
- eval_report_error(state);
- }
- }
-
- return false;
-}
-
-/**
- * -nouser test.
- */
-bool eval_nouser(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- errno = 0;
- if (getpwuid(statbuf->uid) == NULL) {
- if (errno == 0) {
- return true;
- } else {
- eval_report_error(state);
- }
- }
-
- return false;
-}
-
-/**
- * -delete action.
- */
-bool eval_delete(const struct expr *expr, struct eval_state *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 bftw_typeflag type = bftw_typeflag(ftwbuf, BFS_STAT_NOFOLLOW);
- if (type == BFTW_DIR) {
- flag |= AT_REMOVEDIR;
- } else if (type == BFTW_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 expr *expr, const struct cmdline *cmdline) {
- int ret = 0;
- if (expr->execbuf && bfs_exec_finish(expr->execbuf) != 0) {
- if (errno != 0) {
- bfs_error(cmdline, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
- }
- ret = -1;
- }
- if (expr->lhs && eval_exec_finish(expr->lhs, cmdline) != 0) {
- ret = -1;
- }
- if (expr->rhs && eval_exec_finish(expr->rhs, cmdline) != 0) {
- ret = -1;
- }
- return ret;
-}
-
-/**
- * -exec[dir]/-ok[dir] actions.
- */
-bool eval_exec(const struct expr *expr, struct eval_state *state) {
- bool ret = bfs_exec(expr->execbuf, state->ftwbuf) == 0;
- if (errno != 0) {
- eval_error(state, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
- *state->ret = EXIT_FAILURE;
- }
- return ret;
-}
-
-/**
- * -exit action.
- */
-bool eval_exit(const struct expr *expr, struct eval_state *state) {
- state->action = BFTW_STOP;
- *state->ret = expr->idata;
- state->quit = true;
- return true;
-}
-
-/**
- * -depth N test.
- */
-bool eval_depth(const struct expr *expr, struct eval_state *state) {
- return expr_cmp(expr, state->ftwbuf->depth);
-}
-
-/**
- * -empty test.
- */
-bool eval_empty(const struct expr *expr, struct eval_state *state) {
- bool ret = false;
- const struct BFTW *ftwbuf = state->ftwbuf;
-
- if (ftwbuf->typeflag == BFTW_DIR) {
- int dfd = openat(ftwbuf->at_fd, ftwbuf->at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
- if (dfd < 0) {
- eval_report_error(state);
- goto done;
- }
-
- DIR *dir = fdopendir(dfd);
- if (!dir) {
- eval_report_error(state);
- close(dfd);
- goto done;
- }
-
- struct dirent *de;
- if (xreaddir(dir, &de) == 0) {
- ret = !de;
- } else {
- eval_report_error(state);
- }
-
- closedir(dir);
- } else {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (statbuf) {
- ret = statbuf->size == 0;
- }
- }
-
-done:
- return ret;
-}
-
-/**
- * -fstype test.
- */
-bool eval_fstype(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- const char *type = bfs_fstype(state->cmdline->mtab, statbuf);
- return strcmp(type, expr->sdata) == 0;
-}
-
-/**
- * -hidden test.
- */
-bool eval_hidden(const struct expr *expr, struct eval_state *state) {
- const struct BFTW *ftwbuf = state->ftwbuf;
- return ftwbuf->nameoff > 0 && ftwbuf->path[ftwbuf->nameoff] == '.';
-}
-
-/**
- * -nohidden action.
- */
-bool eval_nohidden(const struct expr *expr, struct eval_state *state) {
- if (eval_hidden(expr, state)) {
- eval_prune(expr, state);
- return false;
- } else {
- return true;
- }
-}
-
-/**
- * -inum test.
- */
-bool eval_inum(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- return expr_cmp(expr, statbuf->ino);
-}
-
-/**
- * -links test.
- */
-bool eval_links(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- return expr_cmp(expr, statbuf->nlink);
-}
-
-/**
- * -i?lname test.
- */
-bool eval_lname(const struct expr *expr, struct eval_state *state) {
- bool ret = false;
- char *name = NULL;
-
- const struct BFTW *ftwbuf = state->ftwbuf;
- if (ftwbuf->typeflag != BFTW_LNK) {
- goto done;
- }
-
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- goto done;
- }
-
- name = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, statbuf->size);
- if (!name) {
- eval_report_error(state);
- goto done;
- }
-
- ret = fnmatch(expr->sdata, name, expr->idata) == 0;
-
-done:
- free(name);
- return ret;
-}
-
-/**
- * -i?name test.
- */
-bool eval_name(const struct expr *expr, struct eval_state *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->sdata, name, expr->idata) == 0;
- free(copy);
- return ret;
-}
-
-/**
- * -i?path test.
- */
-bool eval_path(const struct expr *expr, struct eval_state *state) {
- const struct BFTW *ftwbuf = state->ftwbuf;
- return fnmatch(expr->sdata, ftwbuf->path, expr->idata) == 0;
-}
-
-/**
- * -perm test.
- */
-bool eval_perm(const struct expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- mode_t mode = statbuf->mode;
- mode_t target;
- if (state->ftwbuf->typeflag == BFTW_DIR) {
- target = expr->dir_mode;
- } else {
- target = expr->file_mode;
- }
-
- switch (expr->mode_cmp) {
- case MODE_EXACT:
- return (mode & 07777) == target;
-
- case MODE_ALL:
- return (mode & target) == target;
-
- case MODE_ANY:
- return !(mode & target) == !target;
- }
-
- return false;
-}
-
-/**
- * -f?ls action.
- */
-bool eval_fls(const struct expr *expr, struct eval_state *state) {
- CFILE *cfile = expr->cfile;
- FILE *file = cfile->file;
- 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 blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 1023)/1024;
- char mode[11];
- format_mode(statbuf->mode, mode);
- uintmax_t nlink = statbuf->nlink;
- if (fprintf(file, "%9ju %6ju %s %3ju ", ino, blocks, mode, nlink) < 0) {
- goto error;
- }
-
- uintmax_t uid = statbuf->uid;
- struct passwd *pwd = getpwuid(uid);
- 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;
- struct group *grp = getgrgid(gid);
- if (grp) {
- if (fprintf(file, " %-8s", grp->gr_name) < 0) {
- goto error;
- }
- } else {
- if (fprintf(file, " %-8ju", gid) < 0) {
- goto error;
- }
- }
-
- if (ftwbuf->typeflag & (BFTW_BLK | BFTW_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->typeflag == BFTW_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 expr *expr, struct eval_state *state) {
- if (cfprintf(expr->cfile, "%pP\n", state->ftwbuf) < 0) {
- eval_report_error(state);
- }
- return true;
-}
-
-/**
- * -f?print0 action.
- */
-bool eval_fprint0(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
- if (bfs_printf(expr->cfile->file, expr->printf, state->ftwbuf) != 0) {
- eval_report_error(state);
- }
-
- return true;
-}
-
-/**
- * -printx action.
- */
-bool eval_fprintx(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
- state->action = BFTW_PRUNE;
- return true;
-}
-
-/**
- * -quit action.
- */
-bool eval_quit(const struct expr *expr, struct eval_state *state) {
- state->action = BFTW_STOP;
- state->quit = true;
- return true;
-}
-
-/**
- * -i?regex test.
- */
-bool eval_regex(const struct expr *expr, struct eval_state *state) {
- const char *path = state->ftwbuf->path;
- size_t len = strlen(path);
- regmatch_t match = {
- .rm_so = 0,
- .rm_eo = len,
- };
-
- int flags = 0;
-#ifdef REG_STARTEND
- flags |= REG_STARTEND;
-#endif
- int err = regexec(expr->regex, path, 1, &match, flags);
- if (err == 0) {
- return match.rm_so == 0 && match.rm_eo == len;
- } else if (err != REG_NOMATCH) {
- char *str = xregerror(err, expr->regex);
- if (str) {
- eval_error(state, "%s.\n", str);
- free(str);
- } else {
- perror("xregerror()");
- }
-
- *state->ret = EXIT_FAILURE;
- }
-
- return false;
-}
-
-/**
- * -samefile test.
- */
-bool eval_samefile(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (!statbuf) {
- return false;
- }
-
- static const off_t scales[] = {
- [SIZE_BLOCKS] = 512,
- [SIZE_BYTES] = 1,
- [SIZE_WORDS] = 2,
- [SIZE_KB] = 1024,
- [SIZE_MB] = 1024LL*1024,
- [SIZE_GB] = 1024LL*1024*1024,
- [SIZE_TB] = 1024LL*1024*1024*1024,
- [SIZE_PB] = 1024LL*1024*1024*1024*1024,
- };
-
- off_t scale = scales[expr->size_unit];
- off_t size = (statbuf->size + scale - 1)/scale; // Round up
- return expr_cmp(expr, size);
-}
-
-/**
- * -sparse test.
- */
-bool eval_sparse(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
- return state->ftwbuf->typeflag & expr->idata;
-}
-
-/**
- * -xattr test.
- */
-bool eval_xattr(const struct expr *expr, struct eval_state *state) {
- int ret = bfs_check_xattrs(state->ftwbuf);
- if (ret >= 0) {
- return ret;
- } else {
- eval_report_error(state);
- return false;
- }
-}
-
-/**
- * -xtype test.
- */
-bool eval_xtype(const struct expr *expr, struct eval_state *state) {
- const struct BFTW *ftwbuf = state->ftwbuf;
- enum bfs_stat_flag flags = ftwbuf->stat_flags ^ (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW);
- enum bftw_typeflag type = bftw_typeflag(ftwbuf, flags);
- if (type == BFTW_ERROR) {
- eval_report_error(state);
- return false;
- } else {
- return type & expr->idata;
- }
-}
-
-#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 timespec *ts) {
-#ifdef BFS_CLOCK
- int ret = clock_gettime(BFS_CLOCK, ts);
- if (ret != 0) {
- perror("clock_gettime()");
- }
- return ret;
-#else
- return -1;
-#endif
-}
-
-/**
- * Record the time that elapsed evaluating an expression.
- */
-static void add_elapsed(struct expr *expr, const struct timespec *start, const struct timespec *end) {
- expr->elapsed.tv_sec += end->tv_sec - start->tv_sec;
- expr->elapsed.tv_nsec += end->tv_nsec - start->tv_nsec;
- if (expr->elapsed.tv_nsec < 0) {
- expr->elapsed.tv_nsec += 1000000000L;
- --expr->elapsed.tv_sec;
- } else if (expr->elapsed.tv_nsec >= 1000000000L) {
- expr->elapsed.tv_nsec -= 1000000000L;
- ++expr->elapsed.tv_sec;
- }
-}
-
-/**
- * Evaluate an expression.
- */
-static bool eval_expr(struct expr *expr, struct eval_state *state) {
- struct timespec start, end;
- bool time = state->cmdline->debug & DEBUG_RATES;
- if (time) {
- if (eval_gettime(&start) != 0) {
- time = false;
- }
- }
-
- assert(!state->quit);
-
- bool ret = expr->eval(expr, state);
-
- if (time) {
- if (eval_gettime(&end) == 0) {
- add_elapsed(expr, &start, &end);
- }
- }
-
- ++expr->evaluations;
- if (ret) {
- ++expr->successes;
- }
-
- if (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 expr *expr, struct eval_state *state) {
- return !eval_expr(expr->rhs, state);
-}
-
-/**
- * Evaluate a conjunction.
- */
-bool eval_and(const struct expr *expr, struct eval_state *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 expr *expr, struct eval_state *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 expr *expr, struct eval_state *state) {
- eval_expr(expr->lhs, state);
-
- if (state->quit) {
- return false;
- }
-
- return eval_expr(expr->rhs, state);
-}
-
-/** Check if we've seen a file before. */
-static bool eval_file_unique(struct eval_state *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 BFTW *ftwbuf, const struct bftw_stat *cache, enum bfs_stat_flag flags) {
- 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 BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf;
- if (statbuf || ftwbuf->stat_cache.error) {
- debug_stat(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(ftwbuf, &ftwbuf->lstat_cache, BFS_STAT_NOFOLLOW);
- }
-}
-
-/**
- * Dump the bftw_typeflag for -D search.
- */
-static const char *dump_bftw_typeflag(enum bftw_typeflag type) {
-#define DUMP_BFTW_TYPEFLAG_CASE(flag) \
- case flag: \
- return #flag
-
- switch (type) {
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_BLK);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_CHR);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_DIR);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_DOOR);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_FIFO);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_LNK);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_PORT);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_REG);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_SOCK);
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_WHT);
-
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_ERROR);
-
- default:
- DUMP_BFTW_TYPEFLAG_CASE(BFTW_UNKNOWN);
- }
-}
-
-#define DUMP_BFTW_MAP(value) [value] = #value
-
-/**
- * Dump the bftw_visit for -D search.
- */
-static const char *dump_bftw_visit(enum bftw_visit visit) {
- static const char *visits[] = {
- DUMP_BFTW_MAP(BFTW_PRE),
- DUMP_BFTW_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_BFTW_MAP(BFTW_CONTINUE),
- DUMP_BFTW_MAP(BFTW_PRUNE),
- DUMP_BFTW_MAP(BFTW_STOP),
- };
- return actions[action];
-}
-
-/**
- * Type passed as the argument to the bftw() callback.
- */
-struct callback_args {
- /** The parsed command line. */
- const struct cmdline *cmdline;
- /** The set of seen files. */
- struct trie *seen;
- /** Eventual return value from eval_cmdline(). */
- int ret;
-};
-
-/**
- * bftw() callback.
- */
-static enum bftw_action cmdline_callback(const struct BFTW *ftwbuf, void *ptr) {
- struct callback_args *args = ptr;
-
- const struct cmdline *cmdline = args->cmdline;
-
- struct eval_state state;
- state.ftwbuf = ftwbuf;
- state.cmdline = cmdline;
- state.action = BFTW_CONTINUE;
- state.ret = &args->ret;
- state.quit = false;
-
- if (ftwbuf->typeflag == BFTW_ERROR) {
- if (!eval_should_ignore(&state, ftwbuf->error)) {
- args->ret = EXIT_FAILURE;
- eval_error(&state, "%s.\n", strerror(ftwbuf->error));
- }
- state.action = BFTW_PRUNE;
- goto done;
- }
-
- if (cmdline->unique && ftwbuf->visit == BFTW_PRE) {
- if (!eval_file_unique(&state, args->seen)) {
- goto done;
- }
- }
-
- if (cmdline->xargs_safe && strpbrk(ftwbuf->path, " \t\n\'\"\\")) {
- args->ret = EXIT_FAILURE;
- eval_error(&state, "Path is not safe for xargs.\n");
- state.action = BFTW_PRUNE;
- goto done;
- }
-
- if (cmdline->maxdepth < 0 || ftwbuf->depth >= cmdline->maxdepth) {
- state.action = BFTW_PRUNE;
- }
-
- // In -depth mode, only handle directories on the BFTW_POST visit
- enum bftw_visit expected_visit = BFTW_PRE;
- if ((cmdline->flags & BFTW_DEPTH)
- && (cmdline->strategy == BFTW_IDS || ftwbuf->typeflag == BFTW_DIR)
- && ftwbuf->depth < cmdline->maxdepth) {
- expected_visit = BFTW_POST;
- }
-
- if (ftwbuf->visit == expected_visit
- && ftwbuf->depth >= cmdline->mindepth
- && ftwbuf->depth <= cmdline->maxdepth) {
- eval_expr(cmdline->expr, &state);
- }
-
-done:
- if (cmdline->debug & DEBUG_STAT) {
- debug_stats(ftwbuf);
- }
-
- if (cmdline->debug & DEBUG_SEARCH) {
- fprintf(stderr, "cmdline_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.typeflag = %s,\n", dump_bftw_typeflag(ftwbuf->typeflag));
- fprintf(stderr, "\t.error = %d,\n", ftwbuf->error);
- fprintf(stderr, "}) == %s\n", dump_bftw_action(state.action));
- }
-
- return state.action;
-}
-
-/**
- * Infer the number of open file descriptors we're allowed to have.
- */
-static int infer_fdlimit(const struct cmdline *cmdline) {
- int ret = 4096;
-
- struct rlimit rl;
- if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
- if (rl.rlim_cur != RLIM_INFINITY) {
- ret = rl.rlim_cur;
- }
- }
-
- // 3 for std{in,out,err}
- int nopen = 3 + cmdline->nopen_files;
-
- // Check /proc/self/fd for the current number of open fds, if possible
- // (we may have inherited more than just the standard ones)
- DIR *dir = opendir("/proc/self/fd");
- if (!dir) {
- dir = opendir("/dev/fd");
- }
- if (dir) {
- // Account for 'dir' itself
- nopen = -1;
-
- struct dirent *de;
- while (xreaddir(dir, &de) == 0 && de) {
- ++nopen;
- }
-
- closedir(dir);
- }
-
- ret -= nopen;
- ret -= cmdline->expr->persistent_fds;
- ret -= cmdline->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_DEPTH);
- DEBUG_FLAG(flags, BFTW_COMFOLLOW);
- DEBUG_FLAG(flags, BFTW_LOGICAL);
- DEBUG_FLAG(flags, BFTW_DETECT_CYCLES);
- DEBUG_FLAG(flags, BFTW_XDEV);
-
- assert(!flags);
-}
-
-/**
- * Dump the bftw_strategy for -D search.
- */
-static const char *dump_bftw_strategy(enum bftw_strategy strategy) {
- static const char *strategies[] = {
- DUMP_BFTW_MAP(BFTW_BFS),
- DUMP_BFTW_MAP(BFTW_DFS),
- DUMP_BFTW_MAP(BFTW_IDS),
- };
- return strategies[strategy];
-}
-
-/**
- * Evaluate the command line.
- */
-int eval_cmdline(const struct cmdline *cmdline) {
- if (!cmdline->expr) {
- return EXIT_SUCCESS;
- }
-
- struct callback_args args = {
- .cmdline = cmdline,
- .ret = EXIT_SUCCESS,
- };
-
- struct trie seen;
- if (cmdline->unique) {
- trie_init(&seen);
- args.seen = &seen;
- }
-
- struct bftw_args bftw_args = {
- .paths = cmdline->paths,
- .npaths = cmdline->npaths,
- .callback = cmdline_callback,
- .ptr = &args,
- .nopenfd = infer_fdlimit(cmdline),
- .flags = cmdline->flags,
- .strategy = cmdline->strategy,
- .mtab = cmdline->mtab,
- };
-
- if (cmdline->debug & DEBUG_SEARCH) {
- fprintf(stderr, "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 = cmdline_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, "cmdline->mtab");
- } else {
- fprintf(stderr, "NULL");
- }
- fprintf(stderr, ",\n})\n");
- }
-
- if (bftw(&bftw_args) != 0) {
- args.ret = EXIT_FAILURE;
- perror("bftw()");
- }
-
- if (eval_exec_finish(cmdline->expr, cmdline) != 0) {
- args.ret = EXIT_FAILURE;
- }
-
- if (cmdline->debug & DEBUG_RATES) {
- dump_cmdline(cmdline, true);
- }
-
- if (cmdline->unique) {
- trie_destroy(&seen);
- }
-
- return args.ret;
-}
diff --git a/eval.h b/eval.h
deleted file mode 100644
index 7038bb9..0000000
--- a/eval.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2018 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 "expr.h"
-
-// Predicate evaluation functions
-bool eval_true(const struct expr *expr, struct eval_state *state);
-bool eval_false(const struct expr *expr, struct eval_state *state);
-
-bool eval_access(const struct expr *expr, struct eval_state *state);
-bool eval_acl(const struct expr *expr, struct eval_state *state);
-bool eval_capable(const struct expr *expr, struct eval_state *state);
-bool eval_perm(const struct expr *expr, struct eval_state *state);
-bool eval_xattr(const struct expr *expr, struct eval_state *state);
-
-bool eval_newer(const struct expr *expr, struct eval_state *state);
-bool eval_time(const struct expr *expr, struct eval_state *state);
-bool eval_used(const struct expr *expr, struct eval_state *state);
-
-bool eval_gid(const struct expr *expr, struct eval_state *state);
-bool eval_uid(const struct expr *expr, struct eval_state *state);
-bool eval_nogroup(const struct expr *expr, struct eval_state *state);
-bool eval_nouser(const struct expr *expr, struct eval_state *state);
-
-bool eval_depth(const struct expr *expr, struct eval_state *state);
-bool eval_empty(const struct expr *expr, struct eval_state *state);
-bool eval_fstype(const struct expr *expr, struct eval_state *state);
-bool eval_hidden(const struct expr *expr, struct eval_state *state);
-bool eval_inum(const struct expr *expr, struct eval_state *state);
-bool eval_links(const struct expr *expr, struct eval_state *state);
-bool eval_samefile(const struct expr *expr, struct eval_state *state);
-bool eval_size(const struct expr *expr, struct eval_state *state);
-bool eval_sparse(const struct expr *expr, struct eval_state *state);
-bool eval_type(const struct expr *expr, struct eval_state *state);
-bool eval_xtype(const struct expr *expr, struct eval_state *state);
-
-bool eval_lname(const struct expr *expr, struct eval_state *state);
-bool eval_name(const struct expr *expr, struct eval_state *state);
-bool eval_path(const struct expr *expr, struct eval_state *state);
-bool eval_regex(const struct expr *expr, struct eval_state *state);
-
-bool eval_delete(const struct expr *expr, struct eval_state *state);
-bool eval_exec(const struct expr *expr, struct eval_state *state);
-bool eval_exit(const struct expr *expr, struct eval_state *state);
-bool eval_nohidden(const struct expr *expr, struct eval_state *state);
-bool eval_fls(const struct expr *expr, struct eval_state *state);
-bool eval_fprint(const struct expr *expr, struct eval_state *state);
-bool eval_fprint0(const struct expr *expr, struct eval_state *state);
-bool eval_fprintf(const struct expr *expr, struct eval_state *state);
-bool eval_fprintx(const struct expr *expr, struct eval_state *state);
-bool eval_prune(const struct expr *expr, struct eval_state *state);
-bool eval_quit(const struct expr *expr, struct eval_state *state);
-
-// Operator evaluation functions
-bool eval_not(const struct expr *expr, struct eval_state *state);
-bool eval_and(const struct expr *expr, struct eval_state *state);
-bool eval_or(const struct expr *expr, struct eval_state *state);
-bool eval_comma(const struct expr *expr, struct eval_state *state);
-
-#endif // BFS_EVAL_H
diff --git a/exec.c b/exec.c
deleted file mode 100644
index ddca2f9..0000000
--- a/exec.c
+++ /dev/null
@@ -1,618 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2018 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 "cmdline.h"
-#include "color.h"
-#include "diag.h"
-#include "dstring.h"
-#include "spawn.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-/** Print some debugging info. */
-static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) {
- if (!(execbuf->flags & BFS_EXEC_DEBUG)) {
- return;
- }
-
- if (execbuf->flags & BFS_EXEC_CONFIRM) {
- fputs("-ok", stderr);
- } else {
- fputs("-exec", stderr);
- }
- if (execbuf->flags & BFS_EXEC_CHDIR) {
- fputs("dir", stderr);
- }
- fputs(": ", stderr);
-
- va_list args;
- va_start(args, format);
- vfprintf(stderr, format, args);
- va_end(args);
-}
-
-extern char **environ;
-
-/** 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*1024*1024)
-
-/** 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
- 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;
-}
-
-struct bfs_exec *parse_bfs_exec(char **argv, enum bfs_exec_flags flags, const struct cmdline *cmdline) {
- struct bfs_exec *execbuf = malloc(sizeof(*execbuf));
- if (!execbuf) {
- perror("malloc()");
- goto fail;
- }
-
- execbuf->flags = flags;
- execbuf->argv = NULL;
- execbuf->argc = 0;
- execbuf->argv_cap = 0;
- execbuf->arg_size = 0;
- execbuf->arg_max = 0;
- execbuf->wd_fd = -1;
- execbuf->wd_path = NULL;
- execbuf->wd_len = 0;
- execbuf->ret = 0;
-
- if (cmdline->debug & DEBUG_EXEC) {
- execbuf->flags |= BFS_EXEC_DEBUG;
- }
-
- size_t i;
- for (i = 1; ; ++i) {
- const char *arg = argv[i];
- if (!arg) {
- if (execbuf->flags & BFS_EXEC_CONFIRM) {
- bfs_error(cmdline, "%s: Expected '... ;'.\n", argv[0]);
- } else {
- bfs_error(cmdline, "%s: Expected '... ;' or '... {} +'.\n", argv[0]);
- }
- goto fail;
- } else if (strcmp(arg, ";") == 0) {
- break;
- } else if (strcmp(arg, "+") == 0) {
- if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(argv[i - 1], "{}") == 0) {
- execbuf->flags |= BFS_EXEC_MULTI;
- break;
- }
- }
- }
-
- execbuf->tmpl_argv = argv + 1;
- execbuf->tmpl_argc = i - 1;
-
- if (execbuf->tmpl_argc == 0) {
- bfs_error(cmdline, "%s: Missing command.\n", argv[0]);
- goto fail;
- }
-
- execbuf->argv_cap = execbuf->tmpl_argc + 1;
- execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv));
- if (!execbuf->argv) {
- perror("malloc()");
- goto fail;
- }
-
- if (execbuf->flags & BFS_EXEC_MULTI) {
- for (i = 0; i < execbuf->tmpl_argc - 1; ++i) {
- char *arg = execbuf->tmpl_argv[i];
- if (strstr(arg, "{}")) {
- bfs_error(cmdline, "%s ... +: Only one '{}' is supported.\n", argv[0]);
- goto fail;
- }
- execbuf->argv[i] = arg;
- }
- execbuf->argc = execbuf->tmpl_argc - 1;
-
- execbuf->arg_max = bfs_exec_arg_max(execbuf);
- }
-
- return execbuf;
-
-fail:
- free_bfs_exec(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 int bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) {
- int ret = 0;
-
- if (execbuf->wd_fd >= 0) {
- if (!ftwbuf || execbuf->wd_fd != ftwbuf->at_fd) {
- ret = close(execbuf->wd_fd);
- }
- execbuf->wd_fd = -1;
- }
-
- if (execbuf->wd_path) {
- free(execbuf->wd_path);
- execbuf->wd_path = NULL;
- execbuf->wd_len = 0;
- }
-
- return ret;
-}
-
-/** 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) {
- fprintf(stderr, "%s ", execbuf->argv[i]);
- }
- fprintf(stderr, "? ");
-
- if (ynprompt() <= 0) {
- errno = 0;
- return -1;
- }
- }
-
- 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;
- }
-
- 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, environ);
-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);
- bfs_exec_debug(execbuf, "Command '%s' terminated by signal %d\n", execbuf->argv[0], sig);
- } else {
- bfs_exec_debug(execbuf, "Command '%s' 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;
-}
-
-/** 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 || error != E2BIG) {
- break;
- }
-
- // Try to recover from E2BIG by trying fewer and fewer arguments
- // until they fit
- bfs_exec_debug(execbuf, "Got E2BIG, shrinking argument list...\n");
- 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;
- size_t new_size = execbuf->arg_size;
-
- 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) {
- execbuf->arg_max = new_size;
- bfs_exec_debug(execbuf, "ARG_MAX: %zu\n", execbuf->arg_max);
-
- // 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 next_size = execbuf->arg_size + bfs_exec_arg_size(arg);
- if (next_size > execbuf->arg_max) {
- bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n",
- next_size, execbuf->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 free_bfs_exec(struct bfs_exec *execbuf) {
- if (execbuf) {
- bfs_exec_closewd(execbuf, NULL);
- free(execbuf->argv);
- free(execbuf);
- }
-}
diff --git a/exec.h b/exec.h
deleted file mode 100644
index 78cdbd2..0000000
--- a/exec.h
+++ /dev/null
@@ -1,118 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017 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 "bftw.h"
-#include "color.h"
-
-struct cmdline;
-
-/**
- * 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,
- /** Print debugging information (-D exec). */
- BFS_EXEC_DEBUG = 1 << 3,
-};
-
-/**
- * Buffer for a command line to be executed.
- */
-struct bfs_exec {
- /** Flags for this exec buffer. */
- enum bfs_exec_flags flags;
-
- /** 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;
-
- /** 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 cmdline
- * The command line.
- * @return The parsed exec action, or NULL on failure.
- */
-struct bfs_exec *parse_bfs_exec(char **argv, enum bfs_exec_flags flags, const struct cmdline *cmdline);
-
-/**
- * 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 free_bfs_exec(struct bfs_exec *execbuf);
-
-#endif // BFS_EXEC_H
diff --git a/expr.h b/expr.h
deleted file mode 100644
index 0251d6b..0000000
--- a/expr.h
+++ /dev/null
@@ -1,223 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2018 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 "exec.h"
-#include "printf.h"
-#include "stat.h"
-#include <regex.h>
-#include <stdbool.h>
-#include <stddef.h>
-#include <sys/types.h>
-#include <time.h>
-
-/**
- * A command line expression.
- */
-struct expr;
-
-/**
- * Ephemeral state for evaluating an expression.
- */
-struct eval_state;
-
-/**
- * Expression evaluation function.
- *
- * @param expr
- * The current expression.
- * @param state
- * The current evaluation state.
- * @return
- * The result of the test.
- */
-typedef bool eval_fn(const struct expr *expr, struct eval_state *state);
-
-/**
- * Possible types of numeric comparison.
- */
-enum cmp_flag {
- /** Exactly n. */
- CMP_EXACT,
- /** Less than n. */
- CMP_LESS,
- /** Greater than n. */
- CMP_GREATER,
-};
-
-/**
- * Possible types of mode comparison.
- */
-enum mode_cmp {
- /** Mode is an exact match (MODE). */
- MODE_EXACT,
- /** Mode has all these bits (-MODE). */
- MODE_ALL,
- /** Mode has any of these bits (/MODE). */
- MODE_ANY,
-};
-
-/**
- * Possible time units.
- */
-enum time_unit {
- /** Minutes. */
- MINUTES,
- /** Days. */
- DAYS,
-};
-
-/**
- * Possible file size units.
- */
-enum size_unit {
- /** 512-byte blocks. */
- SIZE_BLOCKS,
- /** Single bytes. */
- SIZE_BYTES,
- /** Two-byte words. */
- SIZE_WORDS,
- /** Kibibytes. */
- SIZE_KB,
- /** Mebibytes. */
- SIZE_MB,
- /** Gibibytes. */
- SIZE_GB,
- /** Tebibytes. */
- SIZE_TB,
- /** Pebibytes. */
- SIZE_PB,
-};
-
-struct expr {
- /** The function that evaluates this expression. */
- eval_fn *eval;
-
- /** The left hand side of the expression. */
- struct expr *lhs;
- /** The right hand side of the expression. */
- struct expr *rhs;
-
- /** 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;
-
- /** Estimated cost. */
- double cost;
- /** Estimated probability of success. */
- double probability;
- /** Number of times this predicate was executed. */
- size_t evaluations;
- /** Number of times this predicate succeeded. */
- size_t successes;
- /** Total time spent running this predicate. */
- struct timespec elapsed;
-
- /** The number of command line arguments for this expression. */
- size_t argc;
- /** The command line arguments comprising this expression. */
- char **argv;
-
- /** The optional comparison flag. */
- enum cmp_flag cmp_flag;
-
- /** The mode comparison flag. */
- enum 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;
-
- /** The optional stat field to look at. */
- enum bfs_stat_field stat_field;
- /** The optional reference time. */
- struct timespec reftime;
- /** The optional time unit. */
- enum time_unit time_unit;
-
- /** The optional size unit. */
- enum size_unit size_unit;
-
- /** Optional device number for a target file. */
- dev_t dev;
- /** Optional inode number for a target file. */
- ino_t ino;
-
- /** File to output to. */
- CFILE *cfile;
-
- /** Optional compiled regex. */
- regex_t *regex;
-
- /** Optional exec command. */
- struct bfs_exec *execbuf;
-
- /** Optional printf command. */
- struct bfs_printf *printf;
-
- /** Optional integer data for this expression. */
- long long idata;
-
- /** Optional string data for this expression. */
- const char *sdata;
-
- /** 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;
-};
-
-/** Singleton true expression instance. */
-extern struct expr expr_true;
-/** Singleton false expression instance. */
-extern struct expr expr_false;
-
-/**
- * Create a new expression.
- */
-struct expr *new_expr(eval_fn *eval, size_t argc, char **argv);
-
-/**
- * @return Whether expr is known to always quit.
- */
-bool expr_never_returns(const struct expr *expr);
-
-/**
- * @return The result of the comparison for this expression.
- */
-bool expr_cmp(const struct expr *expr, long long n);
-
-/**
- * Dump a parsed expression.
- */
-void dump_expr(CFILE *cfile, const struct expr *expr, bool verbose);
-
-/**
- * Free an expression tree.
- */
-void free_expr(struct expr *expr);
-
-#endif // BFS_EXPR_H
diff --git a/fsade.c b/fsade.c
deleted file mode 100644
index 8f8f8b1..0000000
--- a/fsade.c
+++ /dev/null
@@ -1,297 +0,0 @@
-/****************************************************************************
- * 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. *
- ****************************************************************************/
-
-#include "fsade.h"
-#include "bftw.h"
-#include "dstring.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.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_CAN_CHECK_XATTRS
-# 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
-
-#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 any ACLs of the given type are non-trivial. */
-static int bfs_check_acl_type(const char *path, acl_type_t type) {
- acl_t acl = acl_get_file(path, type);
- if (!acl) {
- if (is_absence_error(errno)) {
- return 0;
- } else {
- return -1;
- }
- }
-
- 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)
- acl_tag_t tag;
- if (acl_get_tag_type(entry, &tag) != 0) {
- continue;
- }
- if (tag != ACL_USER_OBJ && tag != ACL_GROUP_OBJ && tag != ACL_OTHER) {
- ret = 1;
- break;
- }
-#else
- ret = 1;
- break;
-#endif
- }
-
- acl_free(acl);
- return ret;
-}
-
-int bfs_check_acl(const struct BFTW *ftwbuf) {
- if (ftwbuf->typeflag == BFTW_LNK) {
- return 0;
- }
-
- const char *path = fake_at(ftwbuf);
-
- int error = ENOTSUP;
- int ret = -1;
-
-#if __APPLE__
- // macOS gives EINVAL for either of the two standard ACL types,
- // supporting only ACL_TYPE_EXTENDED
- if (ret <= 0) {
- ret = bfs_check_acl_type(path, ACL_TYPE_EXTENDED);
- if (ret < 0) {
- error = errno;
- }
- }
-#else
- if (ret <= 0) {
- ret = bfs_check_acl_type(path, ACL_TYPE_ACCESS);
- if (ret < 0) {
- error = errno;
- }
- }
-
- if (ret <= 0 && ftwbuf->typeflag == BFTW_DIR) {
- ret = bfs_check_acl_type(path, ACL_TYPE_DEFAULT);
- if (ret < 0) {
- error = errno;
- }
- }
-#endif
-
- 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->typeflag == BFTW_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 __APPLE__
- int options = ftwbuf->typeflag == BFTW_LNK ? XATTR_NOFOLLOW : 0;
- len = listxattr(path, NULL, 0, options);
-#else
- if (ftwbuf->typeflag == BFTW_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;
- }
-}
-
-#else // !BFS_CAN_CHECK_XATTRS
-
-int bfs_check_xattrs(const struct BFTW *ftwbuf) {
- errno = ENOTSUP;
- return -1;
-}
-
-#endif
diff --git a/fsade.h b/fsade.h
deleted file mode 100644
index c9dcfc9..0000000
--- a/fsade.h
+++ /dev/null
@@ -1,70 +0,0 @@
-/****************************************************************************
- * 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. *
- ****************************************************************************/
-
-/**
- * A facade over (file)system features that are (un)implemented differently
- * between platforms.
- */
-
-#ifndef BFS_FSADE_H
-#define BFS_FSADE_H
-
-#include "bftw.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_XATTR
-
-/**
- * Check if a file has a non-trvial 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-trvial 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);
-
-#endif // BFS_FSADE_H
diff --git a/main.c b/main.c
deleted file mode 100644
index 2aba5bc..0000000
--- a/main.c
+++ /dev/null
@@ -1,111 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-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. *
- ****************************************************************************/
-
-/**
- * - main(): the entry point for bfs(1), a breadth-first version of find(1)
- * - main.c (this file)
- *
- * - parse_cmdline(): parses the command line into an expression tree
- * - cmdline.h (declares the parsed command line structure)
- * - expr.h (declares the expression tree nodes)
- * - parse.c (the parser itself)
- * - opt.c (the expression optimizer)
- *
- * - eval_cmdline(): 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 eval_cmdline() to walk the directory tree(s)
- * - bftw.[ch] (an extended version of nftw(3))
- *
- * - Utilities:
- * - bfs.h (constants about bfs itself)
- * - color.[ch] (for pretty terminal colors)
- * - diag.[ch] (formats diagnostic messages)
- * - dstring.[ch] (a dynamic string library)
- * - fsade.[ch] (a facade over non-standard filesystem features)
- * - mtab.[ch] (parses the system's mount table)
- * - spawn.[ch] (spawns processes)
- * - 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)
- */
-
-#include "cmdline.h"
-#include "util.h"
-#include <errno.h>
-#include <fcntl.h>
-#include <locale.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-
-/**
- * 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 cmdline *cmdline = parse_cmdline(argc, argv);
- if (cmdline) {
- ret = eval_cmdline(cmdline);
- }
-
- if (free_cmdline(cmdline) != 0 && ret == EXIT_SUCCESS) {
- ret = EXIT_FAILURE;
- }
-
-done:
- return ret;
-}
diff --git a/mtab.c b/mtab.c
deleted file mode 100644
index c21cf0b..0000000
--- a/mtab.c
+++ /dev/null
@@ -1,229 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-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 "mtab.h"
-#include "trie.h"
-#include "util.h"
-#include <errno.h>
-#include <fcntl.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.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
-
-struct bfs_mtab {
- /** A map from device ID to file system type. */
- struct trie types;
- /** The names of all the mount points. */
- struct trie names;
-};
-
-/**
- * Add an entry to the mount table.
- */
-static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, dev_t dev, const char *type) {
- if (!trie_insert_str(&mtab->names, xbasename(path))) {
- return -1;
- }
-
- struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &dev, sizeof(dev));
- if (!leaf) {
- return -1;
- }
-
- if (leaf->value) {
- return 0;
- }
-
- leaf->value = strdup(type);
- if (leaf->value) {
- return 0;
- } else {
- trie_remove(&mtab->types, leaf);
- return -1;
- }
-}
-
-struct bfs_mtab *parse_bfs_mtab() {
-#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
- file = setmntent("/proc/mounts", "r");
- }
- if (!file) {
- goto fail;
- }
-
- struct bfs_mtab *mtab = malloc(sizeof(*mtab));
- if (!mtab) {
- goto fail_file;
- }
- trie_init(&mtab->types);
- trie_init(&mtab->names);
-
- struct mntent *mnt;
- while ((mnt = getmntent(file))) {
- struct bfs_stat sb;
- if (bfs_stat(AT_FDCWD, mnt->mnt_dir, BFS_STAT_NOFOLLOW, &sb) != 0) {
- continue;
- }
-
- if (bfs_mtab_add(mtab, mnt->mnt_dir, sb.dev, mnt->mnt_type) != 0) {
- goto fail_mtab;
- }
- }
-
- endmntent(file);
- return mtab;
-
-fail_mtab:
- free_bfs_mtab(mtab);
-fail_file:
- endmntent(file);
-fail:
- return NULL;
-
-#elif BFS_MNTINFO
-
- struct statfs *mntbuf;
- int size = getmntinfo(&mntbuf, MNT_WAIT);
- if (size < 0) {
- return NULL;
- }
-
- struct bfs_mtab *mtab = malloc(sizeof(*mtab));
- if (!mtab) {
- goto fail;
- }
- trie_init(&mtab->types);
- trie_init(&mtab->names);
-
- for (struct statfs *mnt = mntbuf; mnt < mntbuf + size; ++mnt) {
- struct bfs_stat sb;
- if (bfs_stat(AT_FDCWD, mnt->f_mntonname, BFS_STAT_NOFOLLOW, &sb) != 0) {
- continue;
- }
-
- if (bfs_mtab_add(mtab, mnt->f_mntonname, sb.dev, mnt->f_fstypename) != 0) {
- goto fail_mtab;
- }
- }
-
- return mtab;
-
-fail_mtab:
- free_bfs_mtab(mtab);
-fail:
- return NULL;
-
-#elif BFS_MNTTAB
-
- FILE *file = fopen(MNTTAB, "r");
- if (!file) {
- goto fail;
- }
-
- struct bfs_mtab *mtab = malloc(sizeof(*mtab));
- if (!mtab) {
- goto fail_file;
- }
- trie_init(&mtab->types);
- trie_init(&mtab->names);
-
- struct mnttab mnt;
- while (getmntent(file, &mnt) == 0) {
- struct bfs_stat sb;
- if (bfs_stat(AT_FDCWD, mnt.mnt_mountp, BFS_STAT_NOFOLLOW, &sb) != 0) {
- continue;
- }
-
- if (bfs_mtab_add(mtab, mnt.mnt_mountp, sb.dev, mnt.mnt_fstype) != 0) {
- goto fail_mtab;
- }
- }
-
- fclose(file);
- return mtab;
-
-fail_mtab:
- free_bfs_mtab(mtab);
-fail_file:
- fclose(file);
-fail:
- return NULL;
-
-#else
-
- errno = ENOTSUP;
- return NULL;
-#endif
-}
-
-const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) {
- 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_maybe_mount(const struct bfs_mtab *mtab, const char *path) {
- const char *name = xbasename(path);
- return trie_find_str(&mtab->names, name);
-}
-
-void free_bfs_mtab(struct bfs_mtab *mtab) {
- if (mtab) {
- trie_destroy(&mtab->names);
-
- struct trie_leaf *leaf;
- while ((leaf = trie_first_leaf(&mtab->types))) {
- free(leaf->value);
- trie_remove(&mtab->types, leaf);
- }
- trie_destroy(&mtab->types);
-
- free(mtab);
- }
-}
diff --git a/mtab.h b/mtab.h
deleted file mode 100644
index a12fbaa..0000000
--- a/mtab.h
+++ /dev/null
@@ -1,70 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-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 platform-specific APIs for enumerating mounted filesystems.
- */
-
-#ifndef BFS_MTAB_H
-#define BFS_MTAB_H
-
-#include "stat.h"
-#include <stdbool.h>
-
-/**
- * A file system mount table.
- */
-struct bfs_mtab;
-
-/**
- * Parse the mount table.
- *
- * @return
- * The parsed mount table, or NULL on error.
- */
-struct bfs_mtab *parse_bfs_mtab(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_maybe_mount(const struct bfs_mtab *mtab, const char *path);
-
-/**
- * Free a mount table.
- */
-void free_bfs_mtab(struct bfs_mtab *mtab);
-
-#endif // BFS_MTAB_H
diff --git a/opt.c b/opt.c
deleted file mode 100644
index 019a136..0000000
--- a/opt.c
+++ /dev/null
@@ -1,870 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-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. *
- ****************************************************************************/
-
-/**
- * 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 "cmdline.h"
-#include "color.h"
-#include "eval.h"
-#include "expr.h"
-#include <assert.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stdio.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(ran