diff options
Diffstat (limited to 'src/color.c')
-rw-r--r-- | src/color.c | 1392 |
1 files changed, 1392 insertions, 0 deletions
diff --git a/src/color.c b/src/color.c new file mode 100644 index 0000000..f004bf2 --- /dev/null +++ b/src/color.c @@ -0,0 +1,1392 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "color.h" +#include "alloc.h" +#include "bfstd.h" +#include "bftw.h" +#include "diag.h" +#include "dir.h" +#include "dstring.h" +#include "expr.h" +#include "fsade.h" +#include "stat.h" +#include "trie.h" +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +/** + * An escape sequence, which may contain embedded NUL bytes. + */ +struct esc_seq { + /** The length of the escape sequence. */ + size_t len; + /** The escape sequence iteself, without a terminating NUL. */ + char seq[]; +}; + +/** + * A colored file extension, like `*.tar=01;31`. + */ +struct ext_color { + /** Priority, to disambiguate case-sensitive and insensitive matches. */ + size_t priority; + /** The escape sequence associated with this extension. */ + struct esc_seq *esc; + /** The length of the extension to match. */ + size_t len; + /** Whether the comparison should be case-sensitive. */ + bool case_sensitive; + /** The extension to match (NUL-terminated). */ + char ext[]; +}; + +struct colors { + /** esc_seq allocator. */ + struct varena esc_arena; + /** ext_color allocator. */ + struct varena ext_arena; + + // Known dircolors keys + + struct esc_seq *reset; + struct esc_seq *leftcode; + struct esc_seq *rightcode; + struct esc_seq *endcode; + struct esc_seq *clear_to_eol; + + struct esc_seq *bold; + struct esc_seq *gray; + struct esc_seq *red; + struct esc_seq *green; + struct esc_seq *yellow; + struct esc_seq *blue; + struct esc_seq *magenta; + struct esc_seq *cyan; + struct esc_seq *white; + + struct esc_seq *warning; + struct esc_seq *error; + + struct esc_seq *normal; + + struct esc_seq *file; + struct esc_seq *multi_hard; + struct esc_seq *executable; + struct esc_seq *capable; + struct esc_seq *setgid; + struct esc_seq *setuid; + + struct esc_seq *directory; + struct esc_seq *sticky; + struct esc_seq *other_writable; + struct esc_seq *sticky_other_writable; + + struct esc_seq *link; + struct esc_seq *orphan; + struct esc_seq *missing; + bool link_as_target; + + struct esc_seq *blockdev; + struct esc_seq *chardev; + struct esc_seq *door; + struct esc_seq *pipe; + struct esc_seq *socket; + + /** A mapping from color names (fi, di, ln, etc.) to struct fields. */ + struct trie names; + + /** Number of extensions. */ + size_t ext_count; + /** Longest extension. */ + size_t ext_len; + /** Case-sensitive extension trie. */ + struct trie ext_trie; + /** Case-insensitive extension trie. */ + struct trie iext_trie; +}; + +/** Allocate an escape sequence. */ +static struct esc_seq *new_esc(struct colors *colors, const char *seq, size_t len) { + struct esc_seq *esc = varena_alloc(&colors->esc_arena, len); + if (esc) { + esc->len = len; + memcpy(esc->seq, seq, len); + } + return esc; +} + +/** Free an escape sequence. */ +static void free_esc(struct colors *colors, struct esc_seq *seq) { + varena_free(&colors->esc_arena, seq, seq->len); +} + +/** Initialize a color in the table. */ +static int init_esc(struct colors *colors, const char *name, const char *value, struct esc_seq **field) { + struct esc_seq *esc = NULL; + if (value) { + esc = new_esc(colors, value, strlen(value)); + if (!esc) { + return -1; + } + } + + *field = esc; + + struct trie_leaf *leaf = trie_insert_str(&colors->names, name); + if (!leaf) { + return -1; + } + + leaf->value = field; + return 0; +} + +/** Check if an escape sequence is equal to a string. */ +static bool esc_eq(const struct esc_seq *esc, const char *str, size_t len) { + return esc->len == len && memcmp(esc->seq, str, len) == 0; +} + +/** Get an escape sequence from the table. */ +static struct esc_seq **get_esc(const struct colors *colors, const char *name) { + const struct trie_leaf *leaf = trie_find_str(&colors->names, name); + return leaf ? leaf->value : NULL; +} + +/** Set a named escape sequence. */ +static int set_esc(struct colors *colors, const char *name, char *value) { + struct esc_seq **field = get_esc(colors, name); + if (!field) { + return 0; + } + + if (*field) { + free_esc(colors, *field); + *field = NULL; + } + + if (value) { + *field = new_esc(colors, value, dstrlen(value)); + if (!*field) { + return -1; + } + } + + return 0; +} + +/** Reverse a string, to turn suffix matches into prefix matches. */ +static void ext_reverse(char *ext, size_t len) { + for (size_t i = 0, j = len - 1; len && i < j; ++i, --j) { + char c = ext[i]; + ext[i] = ext[j]; + ext[j] = c; + } +} + +/** Convert a string to lowercase for case-insensitive matching. */ +static void ext_tolower(char *ext, size_t len) { + for (size_t i = 0; i < len; ++i) { + char c = ext[i]; + + // 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 (c >= 'A' && c <= 'Z') { + c += 'a' - 'A'; + } + + ext[i] = c; + } +} + +/** + * The "smart case" algorithm. + * + * @param ext + * The current extension being added. + * @param prev + * The previous case-sensitive match, if any, for the same extension. + * @param iprev + * The previous case-insensitive match, if any, for the same extension. + * @return + * Whether this extension should become case-sensitive. + */ +static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *prev, struct ext_color *iprev) { + // This is the first case-insensitive occurrence of this extension, e.g. + // + // *.gz=01;31:*.tar.gz=01;33 + if (!iprev) { + bfs_assert(!prev); + return false; + } + + // If the last version of this extension is already case-sensitive, + // this one should be too, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33 + if (iprev->case_sensitive) { + return true; + } + + // The case matches the last occurrence exactly, e.g. + // + // *.tar.gz=01;31:*.tar.gz=01;33 + if (iprev == prev) { + return false; + } + + // Different case, but same value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;31 + if (esc_eq(iprev->esc, ext->esc->seq, ext->esc->len)) { + return false; + } + + // Different case, different value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;33 + return true; +} + +/** Set the color for an extension. */ +static int set_ext(struct colors *colors, char *key, char *value) { + size_t len = dstrlen(key); + struct ext_color *ext = varena_alloc(&colors->ext_arena, len + 1); + if (!ext) { + return -1; + } + + ext->priority = colors->ext_count++; + ext->len = len; + ext->case_sensitive = false; + ext->esc = new_esc(colors, value, dstrlen(value)); + if (!ext->esc) { + goto fail; + } + + key = memcpy(ext->ext, key, len + 1); + + // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix() + ext_reverse(key, len); + + // Find any pre-existing exact match + struct ext_color *prev = NULL; + struct trie_leaf *leaf = trie_find_str(&colors->ext_trie, key); + if (leaf) { + prev = leaf->value; + trie_remove(&colors->ext_trie, leaf); + } + + // A later *.x should override any earlier *.x, *.y.x, etc. + while ((leaf = trie_find_postfix(&colors->ext_trie, key))) { + trie_remove(&colors->ext_trie, leaf); + } + + // Insert the extension into the case-sensitive trie + leaf = trie_insert_str(&colors->ext_trie, key); + if (!leaf) { + goto fail; + } + leaf->value = ext; + + // "Smart case": if the same extension is given with two different + // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive + ext_tolower(key, len); + leaf = trie_insert_str(&colors->iext_trie, key); + if (!leaf) { + goto fail; + } + + struct ext_color *iprev = leaf->value; + if (ext_case_sensitive(ext, prev, iprev)) { + iprev->case_sensitive = true; + ext->case_sensitive = true; + } + leaf->value = ext; + + return 0; + +fail: + if (ext->esc) { + free_esc(colors, ext->esc); + } + varena_free(&colors->ext_arena, ext, len + 1); + return -1; +} + +/** Rebuild the case-insensitive trie after all extensions have been parsed. */ +static int build_iext_trie(struct colors *colors) { + trie_clear(&colors->iext_trie); + + for_trie (leaf, &colors->ext_trie) { + size_t len = leaf->length - 1; + if (colors->ext_len < len) { + colors->ext_len = len; + } + + struct ext_color *ext = leaf->value; + if (ext->case_sensitive) { + continue; + } + + // set_ext() already reversed and lowercased the extension + struct trie_leaf *ileaf; + while ((ileaf = trie_find_postfix(&colors->iext_trie, ext->ext))) { + trie_remove(&colors->iext_trie, ileaf); + } + + ileaf = trie_insert_str(&colors->iext_trie, ext->ext); + if (!ileaf) { + return -1; + } + ileaf->value = ext; + } + + return 0; +} + +/** + * Find a color by an extension. + */ +static const struct esc_seq *get_ext(const struct colors *colors, const char *filename) { + size_t ext_len = colors->ext_len; + size_t name_len = strlen(filename); + if (name_len < ext_len) { + ext_len = name_len; + } + const char *suffix = filename + name_len - ext_len; + + char buf[256]; + char *copy; + if (ext_len < sizeof(buf)) { + copy = memcpy(buf, suffix, ext_len + 1); + } else { + copy = strndup(suffix, ext_len); + if (!copy) { + return NULL; + } + } + + ext_reverse(copy, ext_len); + const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_trie, copy); + const struct ext_color *ext = leaf ? leaf->value : NULL; + + ext_tolower(copy, ext_len); + const struct trie_leaf *ileaf = trie_find_prefix(&colors->iext_trie, copy); + const struct ext_color *iext = ileaf ? ileaf->value : NULL; + + if (iext && (!ext || ext->priority < iext->priority)) { + ext = iext; + } + + if (copy != buf) { + free(copy); + } + + return ext ? ext->esc : 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 str + * A dstring to fill with the unescaped chunk. + * @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 + * 0 on success, -1 on failure. + */ +static int unescape(char **str, const char *value, char end, const char **next) { + *next = NULL; + + if (!value) { + errno = EINVAL; + return -1; + } + + if (dstresize(str, 0) != 0) { + return -1; + } + + 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': + errno = EINVAL; + return -1; + + default: + c = *i; + break; + } + break; + + case '^': + switch (*++i) { + case '?': + c = '\177'; + break; + case '\0': + errno = EINVAL; + return -1; + default: + // CTRL masks bits 6 and 7 + c = *i & 0x1F; + break; + } + break; + + default: + c = *i; + break; + } + + if (dstrapp(str, c) != 0) { + return -1; + } + } + + if (*i) { + *next = i + 1; + } + + return 0; +} + +/** Parse the GNU $LS_COLORS format. */ +static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { + int ret = -1; + dchar *key = NULL; + dchar *value = NULL; + + for (const char *chunk = ls_colors, *next; chunk; chunk = next) { + if (chunk[0] == '*') { + if (unescape(&key, chunk + 1, '=', &next) != 0) { + goto fail; + } + if (unescape(&value, next, ':', &next) != 0) { + goto fail; + } + if (set_ext(colors, key, value) != 0) { + goto fail; + } + } else { + const char *equals = strchr(chunk, '='); + if (!equals) { + break; + } + + if (dstrncpy(&key, chunk, equals - chunk) != 0) { + goto fail; + } + if (unescape(&value, equals + 1, ':', &next) != 0) { + goto fail; + } + + // All-zero values should be treated like NULL, to fall + // back on any other relevant coloring for that file + char *esc = value; + if (strspn(value, "0") == strlen(value) + && strcmp(key, "rs") != 0 + && strcmp(key, "lc") != 0 + && strcmp(key, "rc") != 0 + && strcmp(key, "ec") != 0) { + esc = NULL; + } + + if (set_esc(colors, key, esc) != 0) { + goto fail; + } + } + } + + ret = 0; +fail: + dstrfree(value); + dstrfree(key); + return ret; +} + +struct colors *parse_colors(void) { + struct colors *colors = ALLOC(struct colors); + if (!colors) { + return NULL; + } + + VARENA_INIT(&colors->esc_arena, struct esc_seq, seq); + VARENA_INIT(&colors->ext_arena, struct ext_color, ext); + trie_init(&colors->names); + colors->ext_count = 0; + colors->ext_len = 0; + trie_init(&colors->ext_trie); + trie_init(&colors->iext_trie); + + bool fail = false; + + // From man console_codes + + fail = fail || init_esc(colors, "rs", "0", &colors->reset); + fail = fail || init_esc(colors, "lc", "\033[", &colors->leftcode); + fail = fail || init_esc(colors, "rc", "m", &colors->rightcode); + fail = fail || init_esc(colors, "ec", NULL, &colors->endcode); + fail = fail || init_esc(colors, "cl", "\033[K", &colors->clear_to_eol); + + fail = fail || init_esc(colors, "bld", "01;39", &colors->bold); + fail = fail || init_esc(colors, "gry", "01;30", &colors->gray); + fail = fail || init_esc(colors, "red", "01;31", &colors->red); + fail = fail || init_esc(colors, "grn", "01;32", &colors->green); + fail = fail || init_esc(colors, "ylw", "01;33", &colors->yellow); + fail = fail || init_esc(colors, "blu", "01;34", &colors->blue); + fail = fail || init_esc(colors, "mag", "01;35", &colors->magenta); + fail = fail || init_esc(colors, "cyn", "01;36", &colors->cyan); + fail = fail || init_esc(colors, "wht", "01;37", &colors->white); + + fail = fail || init_esc(colors, "wrn", "01;33", &colors->warning); + fail = fail || init_esc(colors, "err", "01;31", &colors->error); + + // Defaults from man dir_colors + // "" means fall back to ->normal + + fail = fail || init_esc(colors, "no", NULL, &colors->normal); + + fail = fail || init_esc(colors, "fi", "", &colors->file); + fail = fail || init_esc(colors, "mh", NULL, &colors->multi_hard); + fail = fail || init_esc(colors, "ex", "01;32", &colors->executable); + fail = fail || init_esc(colors, "ca", NULL, &colors->capable); + fail = fail || init_esc(colors, "sg", "30;43", &colors->setgid); + fail = fail || init_esc(colors, "su", "37;41", &colors->setuid); + + fail = fail || init_esc(colors, "di", "01;34", &colors->directory); + fail = fail || init_esc(colors, "st", "37;44", &colors->sticky); + fail = fail || init_esc(colors, "ow", "34;42", &colors->other_writable); + fail = fail || init_esc(colors, "tw", "30;42", &colors->sticky_other_writable); + + fail = fail || init_esc(colors, "ln", "01;36", &colors->link); + fail = fail || init_esc(colors, "or", NULL, &colors->orphan); + fail = fail || init_esc(colors, "mi", NULL, &colors->missing); + colors->link_as_target = false; + + fail = fail || init_esc(colors, "bd", "01;33", &colors->blockdev); + fail = fail || init_esc(colors, "cd", "01;33", &colors->chardev); + fail = fail || init_esc(colors, "do", "01;35", &colors->door); + fail = fail || init_esc(colors, "pi", "33", &colors->pipe); + fail = fail || init_esc(colors, "so", "01;35", &colors->socket); + + if (fail) { + goto fail; + } + + if (parse_gnu_ls_colors(colors, getenv("LS_COLORS")) != 0) { + goto fail; + } + if (parse_gnu_ls_colors(colors, getenv("BFS_COLORS")) != 0) { + goto fail; + } + if (build_iext_trie(colors) != 0) { + goto fail; + } + + if (colors->link && esc_eq(colors->link, "target", strlen("target"))) { + colors->link_as_target = true; + colors->link->len = 0; + } + + return colors; + +fail: + free_colors(colors); + return NULL; +} + +void free_colors(struct colors *colors) { + if (!colors) { + return; + } + + trie_destroy(&colors->iext_trie); + trie_destroy(&colors->ext_trie); + trie_destroy(&colors->names); + varena_destroy(&colors->ext_arena); + varena_destroy(&colors->esc_arena); + + free(colors); +} + +CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) { + CFILE *cfile = ALLOC(CFILE); + if (!cfile) { + return NULL; + } + + cfile->buffer = dstralloc(128); + if (!cfile->buffer) { + free(cfile); + return NULL; + } + + cfile->file = file; + cfile->need_reset = false; + cfile->close = close; + + if (isatty(fileno(file))) { + cfile->colors = colors; + } else { + cfile->colors = NULL; + } + + return cfile; +} + +int cfclose(CFILE *cfile) { + int ret = 0; + + if (cfile) { + dstrfree(cfile->buffer); + + if (cfile->close) { + ret = fclose(cfile->file); + } + + free(cfile); + } + + return ret; +} + +/** Check if a symlink is broken. */ +static bool is_link_broken(const struct BFTW *ftwbuf) { + if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) { + return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0; + } else { + return true; + } +} + +bool colors_need_stat(const struct colors *colors) { + return colors->setuid || colors->setgid || colors->executable || colors->multi_hard + || colors->sticky_other_writable || colors->other_writable || colors->sticky; +} + +/** Get the color for a file. */ +static const struct esc_seq *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + enum bfs_type type = bftw_type(ftwbuf, flags); + if (type == BFS_ERROR) { + goto error; + } + + const struct bfs_stat *statbuf = NULL; + const struct esc_seq *color = NULL; + + switch (type) { + case BFS_REG: + if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) { + statbuf = bftw_stat(ftwbuf, flags); + if (!statbuf) { + goto error; + } + } + + if (colors->setuid && (statbuf->mode & 04000)) { + color = colors->setuid; + } else if (colors->setgid && (statbuf->mode & 02000)) { + color = colors->setgid; + } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) { + color = colors->capable; + } else if (colors->executable && (statbuf->mode & 00111)) { + color = colors->executable; + } else if (colors->multi_hard && statbuf->nlink > 1) { + color = colors->multi_hard; + } + + if (!color) { + color = get_ext(colors, filename); + } + + if (!color) { + color = colors->file; + } + + break; + + case BFS_DIR: + if (colors->sticky_other_writable || colors->other_writable || colors->sticky) { + statbuf = bftw_stat(ftwbuf, flags); + if (!statbuf) { + goto error; + } + } + + if (colors->sticky_other_writable && (statbuf->mode & 01002) == 01002) { + color = colors->sticky_other_writable; + } else if (colors->other_writable && (statbuf->mode & 00002)) { + color = colors->other_writable; + } else if (colors->sticky && (statbuf->mode & 01000)) { + color = colors->sticky; + } else { + color = colors->directory; + } + + break; + + case BFS_LNK: + if (colors->orphan && is_link_broken(ftwbuf)) { + color = colors->orphan; + } else { + color = colors->link; + } + break; + + case BFS_BLK: + color = colors->blockdev; + break; + case BFS_CHR: + color = colors->chardev; + break; + case BFS_FIFO: + color = colors->pipe; + break; + case BFS_SOCK: + color = colors->socket; + break; + case BFS_DOOR: + color = colors->door; + break; + + default: + break; + } + + if (color && color->len == 0) { + color = colors->normal; + } + + return color; + +error: + if (colors->missing) { + return colors->missing; + } else { + return colors->orphan; + } +} + +/** Print an escape sequence chunk. */ +static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) { + return dstrxcat(&cfile->buffer, esc->seq, esc->len); +} + +/** Print an ANSI escape sequence. */ +static int print_esc(CFILE *cfile, const struct esc_seq *esc) { + if (!esc) { + return 0; + } + + const struct colors *colors = cfile->colors; + if (esc != colors->reset) { + cfile->need_reset = true; + } + + if (print_esc_chunk(cfile, cfile->colors->leftcode) != 0) { + return -1; + } + if (print_esc_chunk(cfile, esc) != 0) { + return -1; + } + if (print_esc_chunk(cfile, cfile->colors->rightcode) != 0) { + return -1; + } + + return 0; +} + +/** Reset after an ANSI escape sequence. */ +static int print_reset(CFILE *cfile) { + if (!cfile->need_reset) { + return 0; + } + cfile->need_reset = false; + + const struct colors *colors = cfile->colors; + if (colors->endcode) { + return print_esc_chunk(cfile, colors->endcode); + } else { + return print_esc(cfile, colors->reset); + } +} + +/** Print a shell-escaped string. */ +static int print_wordesc(CFILE *cfile, const char *str, size_t n, enum wesc_flags flags) { + return dstrnescat(&cfile->buffer, str, n, flags); +} + +/** Print a string with an optional color. */ +static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *str, size_t len) { + if (print_esc(cfile, esc) != 0) { + return -1; + } + + // Don't let the string itself interfere with the colors + if (print_wordesc(cfile, str, len, WESC_TTY) != 0) { + return -1; + } + + if (print_reset(cfile) != 0) { + return -1; + } + + return 0; +} + +/** Find the offset of the first broken path component. */ +static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) { + ssize_t ret = max; + bfs_assert(ret >= 0); + + if (bftw_type(ftwbuf, flags) != BFS_ERROR) { + goto out; + } + + dchar *at_path; + int at_fd; + if (path == ftwbuf->path) { + if (ftwbuf->depth == 0) { + at_fd = AT_FDCWD; + at_path = dstrndup(path, max); + } else { + // The parent must have existed to get here + goto out; + } + } else { + // We're in print_link_target(), so resolve relative to the link's parent directory + at_fd = ftwbuf->at_fd; + if (at_fd == AT_FDCWD && path[0] != '/') { + at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff); + if (at_path && dstrncat(&at_path, path, max) != 0) { + ret = -1; + goto out_path; + } + } else { + at_path = dstrndup(path, max); + } + } + + if (!at_path) { + ret = -1; + goto out; + } + + while (ret > 0) { + if (xfaccessat(at_fd, at_path, F_OK) == 0) { + break; + } + + size_t len = dstrlen(at_path); + while (ret && at_path[len - 1] == '/') { + --len, --ret; + } + if (errno != ENOTDIR) { + while (ret && at_path[len - 1] != '/') { + --len, --ret; + } + } + + dstresize(&at_path, len); + } + +out_path: + dstrfree(at_path); +out: + return ret; +} + +/** Print a path with colors. */ +static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + size_t nameoff; + if (path == ftwbuf->path) { + nameoff = ftwbuf->nameoff; + } else { + nameoff = xbaseoff(path); + } + + const char *name = path + nameoff; + size_t pathlen = nameoff + strlen(name); + + ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); + if (broken < 0) { + return -1; + } + size_t split = broken; + + const struct colors *colors = cfile->colors; + const struct esc_seq *dirs_color = colors->directory; + const struct esc_seq *name_color; + + if (split < nameoff) { + name_color = colors->missing; + if (!name_color) { + name_color = colors->orphan; + } + } else { + name_color = file_color(cfile->colors, path + nameoff, ftwbuf, flags); + if (name_color == dirs_color) { + split = pathlen; + } + } + + if (split > 0) { + if (print_colored(cfile, dirs_color, path, split) != 0) { + return -1; + } + } + + if (split < pathlen) { + if (print_colored(cfile, name_color, path + split, pathlen - split) != 0) { + return -1; + } + } + + return 0; +} + +/** Print a file name with colors. */ +static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + const struct esc_seq *esc = file_color(cfile->colors, name, ftwbuf, flags); + return print_colored(cfile, esc, name, strlen(name)); +} + +/** Print the name of a file with the appropriate colors. */ +static int print_name(CFILE *cfile, const struct BFTW *ftwbuf) { + const char *name = ftwbuf->path + ftwbuf->nameoff; + + const struct colors *colors = cfile->colors; + if (!colors) { + return dstrcat(&cfile->buffer, name); + } + + enum bfs_stat_flags flags = ftwbuf->stat_flags; + if (colors->link_as_target && ftwbuf->type == BFS_LNK) { + flags = BFS_STAT_TRYFOLLOW; + } + + return print_name_colored(cfile, name, ftwbuf, flags); +} + +/** Print the path to a file with the appropriate colors. */ +static int print_path(CFILE *cfile, const struct BFTW *ftwbuf) { + const struct colors *colors = cfile->colors; + if (!colors) { + return dstrcat(&cfile->buffer, ftwbuf->path); + } + + enum bfs_stat_flags flags = ftwbuf->stat_flags; + if (colors->link_as_target && ftwbuf->type == BFS_LNK) { + flags = BFS_STAT_TRYFOLLOW; + } + + return print_path_colored(cfile, ftwbuf->path, ftwbuf, flags); +} + +/** Print a link target with the appropriate colors. */ +static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { + const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW); + size_t len = statbuf ? statbuf->size : 0; + + char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len); + if (!target) { + return -1; + } + + int ret; + if (cfile->colors) { + ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW); + } else { + ret = dstrcat(&cfile->buffer, target); + } + + free(target); + return ret; +} + +/** Format some colored output to the buffer. */ +attr(printf(2, 3)) +static int cbuff(CFILE *cfile, const char *format, ...); + +/** Dump a parsed expression tree, for debugging. */ +static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) { + if (depth >= 2) { + return dstrcat(&cfile->buffer, "(...)"); + } + + if (!expr) { + return dstrcat(&cfile->buffer, "(null)"); + } + + if (dstrcat(&cfile->buffer, "(") != 0) { + return -1; + } + + if (bfs_expr_is_parent(expr)) { + if (cbuff(cfile, "${red}%pq${rs}", expr->argv[0]) < 0) { + return -1; + } + } else { + if (cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]) < 0) { + return -1; + } + } + + for (size_t i = 1; i < expr->argc; ++i) { + if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { + return -1; + } + } + + if (verbose) { + double rate = 0.0, time = 0.0; + if (expr->evaluations) { + rate = 100.0 * expr->successes / expr->evaluations; + time = (1.0e9 * expr->elapsed.tv_sec + expr->elapsed.tv_nsec) / expr->evaluations; + } + if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]", + expr->successes, expr->evaluations, rate, time)) { + return -1; + } + } + + int count = 0; + for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + if (dstrcat(&cfile->buffer, " ") != 0) { + return -1; + } + if (++count >= 3) { + if (dstrcat(&cfile->buffer, "...") != 0) { + return -1; + } + break; + } else { + if (print_expr(cfile, child, verbose, depth + 1) != 0) { + return -1; + } + } + } + + if (dstrcat(&cfile->buffer, ")") != 0) { + return -1; + } + + return 0; +} + +attr(printf(2, 0)) +static int cvbuff(CFILE *cfile, const char *format, va_list args) { + const struct colors *colors = cfile->colors; + int error = errno; + + // Color specifier (e.g. ${blu}) state + struct esc_seq **esc; + const char *end; + size_t len; + char name[4]; + + for (const char *i = format; *i; ++i) { + size_t verbatim = strcspn(i, "%$"); + if (dstrncat(&cfile->buffer, i, verbatim) != 0) { + return -1; + } + i += verbatim; + + switch (*i) { + case '%': + switch (*++i) { + case '%': + if (dstrapp(&cfile->buffer, '%') != 0) { + return -1; + } + break; + + case 'c': + if (dstrapp(&cfile->buffer, va_arg(args, int)) != 0) { + return -1; + } + break; + + case 'd': + if (dstrcatf(&cfile->buffer, "%d", va_arg(args, int)) != 0) { + return -1; + } + break; + + case 'g': + if (dstrcatf(&cfile->buffer, "%g", va_arg(args, double)) != 0) { + return -1; + } + break; + + case 's': + if (dstrcat(&cfile->buffer, va_arg(args, const char *)) != 0) { + return -1; + } + break; + + case 'z': + ++i; + if (*i != 'u') { + goto invalid; + } + if (dstrcatf(&cfile->buffer, "%zu", va_arg(args, size_t)) != 0) { + return -1; + } + break; + + case 'm': + if (dstrcat(&cfile->buffer, xstrerror(error)) != 0) { + return -1; + } + break; + + case 'p': + switch (*++i) { + case 'q': + if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_SHELL | WESC_TTY) != 0) { + return -1; + } + break; + case 'Q': + if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_TTY) != 0) { + return -1; + } + break; + + case 'F': + if (print_name(cfile, va_arg(args, const struct BFTW *)) != 0) { + return -1; + } + break; + + case 'P': + if (print_path(cfile, va_arg(args, const struct BFTW *)) != 0) { + return -1; + } + break; + + case 'L': + if (print_link_target(cfile, va_arg(args, const struct BFTW *)) != 0) { + return -1; + } + break; + + case 'e': + if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false, 0) != 0) { + return -1; + } + break; + case 'E': + if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true, 0) != 0) { + return -1; + } + break; + + default: + goto invalid; + } + + break; + + default: + goto invalid; + } + break; + + case '$': + switch (*++i) { + case '$': + if (dstrapp(&cfile->buffer, '$') != 0) { + return -1; + } + break; + + case '{': + ++i; + end = strchr(i, '}'); + if (!end) { + goto invalid; + } + if (!colors) { + i = end; + break; + } + + len = end - i; + if (len >= sizeof(name)) { + goto invalid; + } + memcpy(name, i, len); + name[len] = '\0'; + + if (strcmp(name, "rs") == 0) { + if (print_reset(cfile) != 0) { + return -1; + } + } else { + esc = get_esc(colors, name); + if (!esc) { + goto invalid; + } + if (print_esc(cfile, *esc) != 0) { + return -1; + } + } + + i = end; + break; + + default: + goto invalid; + } + break; + + default: + return 0; + } + } + + return 0; + +invalid: + bfs_bug("Invalid format string '%s'", format); + errno = EINVAL; + return -1; +} + +static int cbuff(CFILE *cfile, const char *format, ...) { + va_list args; + va_start(args, format); + int ret = cvbuff(cfile, format, args); + va_end(args); + return ret; +} + +int cvfprintf(CFILE *cfile, const char *format, va_list args) { + bfs_assert(dstrlen(cfile->buffer) == 0); + + int ret = -1; + if (cvbuff(cfile, format, args) == 0) { + size_t len = dstrlen(cfile->buffer); + if (fwrite(cfile->buffer, 1, len, cfile->file) == len) { + ret = 0; + } + } + + dstresize(&cfile->buffer, 0); + return ret; +} + +int cfprintf(CFILE *cfile, const char *format, ...) { + va_list args; + va_start(args, format); + int ret = cvfprintf(cfile, format, args); + va_end(args); + return ret; +} |