/**************************************************************************** * bfs * * Copyright (C) 2015-2021 Tavian Barnes * * * * Permission to use, copy, modify, and/or distribute this software for any * * purpose with or without fee is hereby granted. * * * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * ****************************************************************************/ #include "color.h" #include "bftw.h" #include "dir.h" #include "dstring.h" #include "expr.h" #include "fsade.h" #include "stat.h" #include "trie.h" #include "util.h" #include #include #include #include #include #include #include #include #include #include /** * 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 int set_color(struct colors *colors, const char *name, char *value) { char **color = get_color(colors, name); if (color) { dstrfree(*color); *color = value; return 0; } else { return -1; } } /** * Transform a file extension for fast lookups, by reversing and lowercasing it. */ static void extxfrm(char *ext) { size_t len = strlen(ext); for (size_t i = 0; i < len - i; ++i) { char a = ext[i]; char b = ext[len - i - 1]; // What's internationalization? Doesn't matter, this is what // GNU ls does. Luckily, since there's no standard C way to // casefold. Not using tolower() here since it respects the // current locale, which GNU ls doesn't do. if (a >= 'A' && a <= 'Z') { a += 'a' - 'A'; } if (b >= 'A' && b <= 'Z') { b += 'a' - 'A'; } ext[i] = b; ext[len - i - 1] = a; } } /** * Set the color for an extension. */ static int set_ext_color(struct colors *colors, char *key, const char *value) { extxfrm(key); // A later *.x should override any earlier *.x, *.y.x, etc. struct trie_leaf *match; while ((match = trie_find_postfix(&colors->ext_colors, key))) { dstrfree(match->value); trie_remove(&colors->ext_colors, match); } struct trie_leaf *leaf = trie_insert_str(&colors->ext_colors, key); if (leaf) { leaf->value = (char *)value; return 0; } else { return -1; } } /** * Find a color by an extension. */ static const char *get_ext_color(const struct colors *colors, const char *filename) { char *xfrm = strdup(filename); if (!xfrm) { return NULL; } extxfrm(xfrm); const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_colors, xfrm); free(xfrm); if (leaf) { return leaf->value; } else { return NULL; } } /** * Parse a chunk of LS_COLORS that may have escape sequences. The supported * escapes are: * * \a, \b, \f, \n, \r, \t, \v: * As in C * \e: * ESC (\033) * \?: * DEL (\177) * \_: * ' ' (space) * \NNN: * Octal * \xNN: * Hex * ^C: * Control character. * * See man dir_colors. * * @param value * The value to parse. * @param end * The character that marks the end of the chunk. * @param[out] next * Will be set to the next chunk. * @return * The parsed chunk as a dstring. */ static char *unescape(const char *value, char end, const char **next) { if (!value) { goto fail; } char *str = dstralloc(0); if (!str) { goto fail_str; } const char *i; for (i = value; *i && *i != end; ++i) { unsigned char c = 0; switch (*i) { case '\\': switch (*++i) { case 'a': c = '\a'; break; case 'b': c = '\b'; break; case 'e': c = '\033'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'v': c = '\v'; break; case '?': c = '\177'; break; case '_': c = ' '; break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': while (i[1] >= '0' && i[1] <= '7') { c <<= 3; c |= *i++ - '0'; } c <<= 3; c |= *i - '0'; break; case 'X': case 'x': while (true) { if (i[1] >= '0' && i[1] <= '9') { c <<= 4; c |= i[1] - '0'; } else if (i[1] >= 'A' && i[1] <= 'F') { c <<= 4; c |= i[1] - 'A' + 0xA; } else if (i[1] >= 'a' && i[1] <= 'f') { c <<= 4; c |= i[1] - 'a' + 0xA; } else { break; } ++i; } break; case '\0': goto fail_str; default: c = *i; break; } break; case '^': switch (*++i) { case '?': c = '\177'; break; case '\0': goto fail_str; default: // CTRL masks bits 6 and 7 c = *i & 0x1F; break; } break; default: c = *i; break; } if (dstrapp(&str, c) != 0) { goto fail_str; } } if (*i) { *next = i + 1; } else { *next = NULL; } return str; fail_str: dstrfree(str); fail: *next = NULL; return NULL; } 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;39", &colors->bold); ret |= init_color(colors, "gry", "01;30", &colors->gray); ret |= init_color(colors, "red", "01;31", &colors->red); ret |= init_color(colors, "grn", "01;32", &colors->green); ret |= init_color(colors, "ylw", "01;33", &colors->yellow); ret |= init_color(colors, "blu", "01;34", &colors->blue); ret |= init_color(colors, "mag", "01;35", &colors->magenta); ret |= init_color(colors, "cyn", "01;36", &colors->cyan); ret |= init_color(colors, "wht", "01;37", &colors->white); ret |= init_color(colors, "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; } if (set_color(colors, key, value) != 0) { dstrfree(value); } free(key); } } if (colors->link && strcmp(colors->link, "target") == 0) { colors->link_as_target = true; dstrfree(colors->link); colors->link = NULL; } return colors; } void free_colors(struct colors *colors) { if (colors) { struct trie_leaf *leaf; while ((leaf = trie_first_leaf(&colors->ext_colors))) { dstrfree(leaf->value); trie_remove(&colors->ext_colors, leaf); } trie_destroy(&colors->ext_colors); while ((leaf = trie_first_leaf(&colors->names))) { char **field = leaf->value; dstrfree(*field); trie_remove(&colors->names, leaf); } trie_destroy(&colors->names); free(colors); } } CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) { CFILE *cfile = malloc(sizeof(*cfile)); if (!cfile) { return NULL; } cfile->buffer = dstralloc(128); if (!cfile->buffer) { free(cfile); return NULL; } cfile->file = file; cfile->close = close; if (isatty(fileno(file))) { cfile->colors = colors; } else { cfile->colors = NULL; } return cfile; } int cfclose(CFILE *cfile) { int ret = 0; if (cfile) { dstrfree(cfile->buffer); if (cfile->close) { ret = fclose(cfile->file); } free(cfile); } return ret; } /** Check if a symlink is broken. */ static bool is_link_broken(const struct BFTW *ftwbuf) { if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) { return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0; } else { return true; } } /** Get the color for a file. */ static const char *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { enum bfs_type type = bftw_type(ftwbuf, flags); if (type == BFS_ERROR) { goto error; } const struct bfs_stat *statbuf = NULL; const char *color = NULL; switch (type) { case BFS_REG: if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) { statbuf = bftw_stat(ftwbuf, flags); if (!statbuf) { goto error; } } if (colors->setuid && (statbuf->mode & 04000)) { color = colors->setuid; } else if (colors->setgid && (statbuf->mode & 02000)) { color = colors->setgid; } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) { color = colors->capable; } else if (colors->executable && (statbuf->mode & 00111)) { color = colors->executable; } else if (colors->multi_hard && statbuf->nlink > 1) { color = colors->multi_hard; } if (!color) { color = get_ext_color(colors, filename); } if (!color) { color = colors->file; } break; case BFS_DIR: if (colors->sticky_other_writable || colors->other_writable || colors->sticky) { statbuf = bftw_stat(ftwbuf, flags); if (!statbuf) { goto error; } } if (colors->sticky_other_writable && (statbuf->mode & 01002) == 01002) { color = colors->sticky_other_writable; } else if (colors->other_writable && (statbuf->mode & 00002)) { color = colors->other_writable; } else if (colors->sticky && (statbuf->mode & 01000)) { color = colors->sticky; } else { color = colors->directory; } break; case BFS_LNK: if (colors->orphan && is_link_broken(ftwbuf)) { color = colors->orphan; } else { color = colors->link; } break; case BFS_BLK: color = colors->blockdev; break; case BFS_CHR: color = colors->chardev; break; case BFS_FIFO: color = colors->pipe; break; case BFS_SOCK: color = colors->socket; break; case BFS_DOOR: color = colors->door; break; default: break; } if (!color) { color = colors->normal; } return color; error: if (colors->missing) { return colors->missing; } else { return colors->orphan; } } /** Print an ANSI escape sequence. */ static int print_esc(CFILE *cfile, const char *esc) { const struct colors *colors = cfile->colors; if (dstrdcat(&cfile->buffer, colors->leftcode) != 0) { return -1; } if (dstrdcat(&cfile->buffer, esc) != 0) { return -1; } if (dstrdcat(&cfile->buffer, colors->rightcode) != 0) { return -1; } return 0; } /** Reset after an ANSI escape sequence. */ static int print_reset(CFILE *cfile) { const struct colors *colors = cfile->colors; if (colors->endcode) { return dstrdcat(&cfile->buffer, colors->endcode); } else { return print_esc(cfile, colors->reset); } } /** Print a string with an optional color. */ static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t len) { if (esc) { if (print_esc(cfile, esc) != 0) { return -1; } } if (dstrncat(&cfile->buffer, str, len) != 0) { return -1; } if (esc) { if (print_reset(cfile) != 0) { return -1; } } return 0; } /** Find the offset of the first broken path component. */ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) { ssize_t ret = max; assert(ret >= 0); if (bftw_type(ftwbuf, flags) != BFS_ERROR) { goto out; } char *at_path; int at_fd; if (path == ftwbuf->path) { if (ftwbuf->depth == 0) { at_fd = AT_FDCWD; at_path = dstrndup(path, max); } else { // The parent must have existed to get here goto out; } } else { // We're in print_link_target(), so resolve relative to the link's parent directory at_fd = ftwbuf->at_fd; if (at_fd == AT_FDCWD && path[0] != '/') { at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff); if (at_path && dstrncat(&at_path, path, max) != 0) { ret = -1; goto out_path; } } else { at_path = dstrndup(path, max); } } if (!at_path) { ret = -1; goto out; } while (ret > 0) { if (xfaccessat(at_fd, at_path, F_OK) == 0) { break; } size_t len = dstrlen(at_path); while (ret && at_path[len - 1] == '/') { --len, --ret; } while (ret && at_path[len - 1] != '/') { --len, --ret; } dstresize(&at_path, len); } out_path: dstrfree(at_path); out: return ret; } /** Print the directories leading up to a file. */ static int print_dirs_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t nameoff) { const struct colors *colors = cfile->colors; ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); if (broken < 0) { return -1; } if (broken > 0) { if (print_colored(cfile, colors->directory, path, broken) != 0) { return -1; } } if ((size_t)broken < nameoff) { const char *color = colors->missing; if (!color) { color = colors->orphan; } if (print_colored(cfile, color, path + broken, nameoff - broken) != 0) { return -1; } } return 0; } /** Print a file name with colors. */ static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { const char *color = file_color(cfile->colors, name, ftwbuf, flags); return print_colored(cfile, color, name, strlen(name)); } /** Print a path with colors. */ static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { size_t nameoff; if (path == ftwbuf->path) { nameoff = ftwbuf->nameoff; } else { nameoff = xbasename(path) - path; } if (print_dirs_colored(cfile, path, ftwbuf, flags, nameoff) != 0) { return -1; } return print_name_colored(cfile, path + nameoff, ftwbuf, flags); } /** Print the name of a file with the appropriate colors. */ static int print_name(CFILE *cfile, const struct BFTW *ftwbuf) { const char *name = ftwbuf->path + ftwbuf->nameoff; const struct colors *colors = cfile->colors; if (!colors) { return dstrcat(&cfile->buffer, name); } enum bfs_stat_flags flags = ftwbuf->stat_flags; if (colors->link_as_target && ftwbuf->type == BFS_LNK) { flags = BFS_STAT_TRYFOLLOW; } return print_name_colored(cfile, name, ftwbuf, flags); } /** Print the path to a file with the appropriate colors. */ static int print_path(CFILE *cfile, const struct BFTW *ftwbuf) { const struct colors *colors = cfile->colors; if (!colors) { return dstrcat(&cfile->buffer, ftwbuf->path); } enum bfs_stat_flags flags = ftwbuf->stat_flags; if (colors->link_as_target && ftwbuf->type == BFS_LNK) { flags = BFS_STAT_TRYFOLLOW; } return print_path_colored(cfile, ftwbuf->path, ftwbuf, flags); } /** Print a link target with the appropriate colors. */ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW); size_t len = statbuf ? statbuf->size : 0; char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len); if (!target) { return -1; } int ret; if (cfile->colors) { ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW); } else { ret = dstrcat(&cfile->buffer, target); } free(target); return ret; } /** Format some colored output to the buffer. */ BFS_FORMATTER(2, 3) static int cbuff(CFILE *cfile, const char *format, ...); /** Dump a parsed expression tree, for debugging. */ static int print_expr(CFILE *cfile, const struct expr *expr, bool verbose) { if (dstrcat(&cfile->buffer, "(") != 0) { return -1; } if (expr->lhs || expr->rhs) { if (cbuff(cfile, "${red}%s${rs}", expr->argv[0]) < 0) { return -1; } } else { if (cbuff(cfile, "${blu}%s${rs}", expr->argv[0]) < 0) { return -1; } } for (size_t i = 1; i < expr->argc; ++i) { if (cbuff(cfile, " ${bld}%s${rs}", expr->argv[i]) < 0) { return -1; } } if (verbose) { double rate = 0.0, time = 0.0; if (expr->evaluations) { rate = 100.0*expr->successes/expr->evaluations; time = (1.0e9*expr->elapsed.tv_sec + expr->elapsed.tv_nsec)/expr->evaluations; } if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]", expr->successes, expr->evaluations, rate, time)) { return -1; } } if (expr->lhs) { if (dstrcat(&cfile->buffer, " ") != 0) { return -1; } if (print_expr(cfile, expr->lhs, verbose) != 0) { return -1; } } if (expr->rhs) { if (dstrcat(&cfile->buffer, " ") != 0) { return -1; } if (print_expr(cfile, expr->rhs, verbose) != 0) { return -1; } } if (dstrcat(&cfile->buffer, ")") != 0) { return -1; } return 0; } static int cvbuff(CFILE *cfile, const char *format, va_list args) { const struct colors *colors = cfile->colors; int error = errno; for (const char *i = format; *i; ++i) { size_t verbatim = strcspn(i, "%$"); if (dstrncat(&cfile->buffer, i, verbatim) != 0) { return -1; } i += verbatim; switch (*i) { case '%': switch (*++i) { case '%': if (dstrapp(&cfile->buffer, '%') != 0) { return -1; } break; case 'c': if (dstrapp(&cfile->buffer, va_arg(args, int)) != 0) { return -1; } break; case 'd': if (dstrcatf(&cfile->buffer, "%d", va_arg(args, int)) != 0) { return -1; } break; case 'g': if (dstrcatf(&cfile->buffer, "%g", va_arg(args, double)) != 0) { return -1; } break; case 's': if (dstrcat(&cfile->buffer, va_arg(args, const char *)) != 0) { return -1; } break; case 'z': ++i; if (*i != 'u') { goto invalid; } if (dstrcatf(&cfile->buffer, "%zu", va_arg(args, size_t)) != 0) { return -1; } break; case 'm': if (dstrcat(&cfile->buffer, strerror(error)) != 0) { return -1; } break; case 'p': switch (*++i) { case 'F': if (print_name(cfile, va_arg(args, const struct BFTW *)) != 0) { return -1; } break; case 'P': if (print_path(cfile, va_arg(args, const struct BFTW *)) != 0) { return -1; } break; case 'L': if (print_link_target(cfile, va_arg(args, const struct BFTW *)) != 0) { return -1; } break; case 'e': if (print_expr(cfile, va_arg(args, const struct expr *), false) != 0) { return -1; } break; case 'E': if (print_expr(cfile, va_arg(args, const struct expr *), true) != 0) { return -1; } break; default: goto invalid; } break; default: goto invalid; } break; case '$': switch (*++i) { case '$': if (dstrapp(&cfile->buffer, '$') != 0) { return -1; } break; case '{': { ++i; const char *end = strchr(i, '}'); if (!end) { goto invalid; } if (!colors) { i = end; break; } size_t len = end - i; char name[len + 1]; memcpy(name, i, len); name[len] = '\0'; char **esc = get_color(colors, name); if (!esc) { goto invalid; } if (*esc) { if (print_esc(cfile, *esc) != 0) { return -1; } } i = end; break; } default: goto invalid; } break; default: return 0; } } return 0; invalid: assert(!"Invalid format string"); errno = EINVAL; return -1; } static int cbuff(CFILE *cfile, const char *format, ...) { va_list args; va_start(args, format); int ret = cvbuff(cfile, format, args); va_end(args); return ret; } int cvfprintf(CFILE *cfile, const char *format, va_list args) { assert(dstrlen(cfile->buffer) == 0); int ret = -1; if (cvbuff(cfile, format, args) == 0) { size_t len = dstrlen(cfile->buffer); if (fwrite(cfile->buffer, 1, len, cfile->file) == len) { ret = 0; } } dstresize(&cfile->buffer, 0); return ret; } int cfprintf(CFILE *cfile, const char *format, ...) { va_list args; va_start(args, format); int ret = cvfprintf(cfile, format, args); va_end(args); return ret; }