commit 8e1fb8d5023e6f5a977b55d5f8cacd7cdec9773f from: Thomas Adam date: Thu Jun 13 10:28:21 2024 UTC mailcap: add parser for external command handling This introduces a (limited) subset of mailcap handling, as defined in RFC 1524. This makes it possible for telescope to run commands on different mime types as defined in a mailcap file, but defaults to a default mime type of using xdg-open if no match is found, or no mailcap file defined. commit - e234161f4be256ceb80d382f93edaa41e06407a8 commit + 8e1fb8d5023e6f5a977b55d5f8cacd7cdec9773f blob - a069064c6c492975fc09a09360434fd1959ab16c blob + 7147d48986f40aa0e63204363d6abd0b4eed8238 --- Makefile.am +++ Makefile.am @@ -35,6 +35,8 @@ telescope_SOURCES = bufio.c \ iri.h \ keymap.c \ keymap.h \ + mailcap.c \ + mailcap.h \ mcache.c \ mcache.h \ mime.c \ blob - /dev/null blob + ae2c7f285ff6bfbf07f821f6c78cef939c2b3d84 (mode 644) --- /dev/null +++ mailcap.c @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2024 Thomas Adam + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * 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. + */ + +/* + * Handles reading mailmap files. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "compat.h" +#include "mailcap.h" + +#define DEFAULT_MAILCAP_ENTRY "*/*; xdg-open %s ; needsterminal" + +#define MAILCAP_NEEDSTERMINAL 0x1 +#define MAILCAP_COPIOUSOUTPUT 0x2 + +#define str_unappend(ch) if (sps.off > 0 && (ch) != EOF) { sps.off--; } + +struct str_parse_state { + char *buf; + size_t len, off; + int esc; +}; +static struct str_parse_state sps; + +static void str_append(char **, size_t *, char); +static int str_getc(void); +static char *str_tokenise(void); +static int str_to_argv(char *, int *, char ***); +static void str_trim_whitespace(char *); +static void argv_free(int, char **); + +struct mailcaplist mailcaps = TAILQ_HEAD_INITIALIZER(mailcaps); + +static FILE *find_mailcap_file(void); +static struct mailcap *mailcap_new(void); +static void mailcap_expand_cmd(struct mailcap *, char *, char *); +static int parse_mailcap_line(char *); +static struct mailcap *mailcap_by_mimetype(const char *); + +/* FIXME: $MAILCAPS can override this, but we don't check for that. */ +static const char *default_mailcaps[] = { + "~/.mailcap", + "/etc/mailcap", + "/usr/etc/mailcap", + "/usr/local/etc/mailcap", + NULL, +}; + +enum mailcap_section { + MAILCAP_MIME = 0, + MAILCAP_CMD, + MAILCAP_FLAGS_1, + MAILCAP_FLAGS_2, + MAILCAP_END_OF_FIELDS, +}; + +static void +str_trim_whitespace(char *s) +{ + char *t; + + s += strspn(s, " \t\n"); + t = s + strlen(s) - 1; + + while (t >= s && isspace((unsigned char)*t)) + *t-- = '\0'; +} + +static void +str_append(char **buf, size_t *len, char add) +{ + size_t al = 1; + + if (al > SIZE_MAX - 1 || *len > SIZE_MAX - 1 - al) + errx(1, "buffer is too big"); + + *buf = realloc(*buf, (*len) + 1 + al); + memcpy((*buf) + *len, &add, al); + (*len) += al; +} + +static int +str_getc(void) +{ + int ch; + + if (sps.esc != 0) { + sps.esc--; + return ('\\'); + } + for (;;) { + ch = EOF; + if (sps.off < sps.len) + ch = sps.buf[sps.off++]; + + if (ch == '\\') { + sps.esc++; + continue; + } + if (ch == '\n' && (sps.esc % 2) == 1) { + sps.esc--; + continue; + } + + if (sps.esc != 0) { + str_unappend(ch); + sps.esc--; + return ('\\'); + } + return (ch); + } +} + +static char * +str_tokenise(void) +{ + int ch; + char *buf; + size_t len; + enum { APPEND, + DOUBLE_QUOTES, + SINGLE_QUOTES, + } state = APPEND; + + len = 0; + buf = calloc(1, sizeof *buf); + + for (;;) { + ch = str_getc(); + /* EOF or \n are always the end of the token. */ + if (ch == EOF || (state == APPEND && ch == '\n')) + break; + + /* Whitespace ends a token unless inside quotes. But if we've + * also been given: + * + * foo "" bar + * + * don't lose that. + */ + if (((ch == ' ' || ch == '\t') && state == APPEND) && + buf[0] != '\0') { + break; + } + + if (ch == '\\' && state != SINGLE_QUOTES) { + ch = str_getc(); + str_append(&buf, &len, ch); + continue; + } + switch (ch) { + case '\'': + if (state == APPEND) { + state = SINGLE_QUOTES; + continue; + } + if (state == SINGLE_QUOTES) { + state = APPEND; + continue; + } + break; + case '"': + if (state == APPEND) { + state = DOUBLE_QUOTES; + continue; + } + if (state == DOUBLE_QUOTES) { + state = APPEND; + continue; + } + break; + default: + /* Otherwise add the character to the buffer. */ + str_append(&buf, &len, ch); + break; + + } + } + str_unappend(ch); + buf[len] = '\0'; + + if (*buf == '\0' || state == SINGLE_QUOTES || state == DOUBLE_QUOTES) { + fprintf(stderr, "Unterminated string: <%s>, missing: %c\n", + buf, state == SINGLE_QUOTES ? '\'' : + state == DOUBLE_QUOTES ? '"' : ' '); + free(buf); + return (NULL); + } + + return (buf); +} + +static int +str_to_argv(char *str, int *ret_argc, char ***ret_argv) +{ + char *token; + int ch, next; + char **argv = NULL; + int argc = 0; + + if (str == NULL) + return -1; + + free(sps.buf); + if ((sps.buf = strdup(str)) == NULL) + errx(1, "strdup"); + sps.len = strlen(sps.buf); + + for (;;) { + if (str[0] == '#') { + /* Skip comment. */ + next = str_getc(); + while (((next = str_getc()) != EOF)) + ; /* Nothing. */ + } + + ch = str_getc(); + + if (ch == '\n' || ch == EOF) + goto out; + if (ch == ' ' || ch == '\t') + continue; + + /* Tokenise the string according to quoting rules. Note that + * the string is stepped-back by one character to make the + * tokenisation easier, and not to kick-off the state of the + * parsing from this point. + */ + str_unappend(ch); + if ((token = str_tokenise()) == NULL) { + argv_free(argc, argv); + return -1; + } + + /* Add to argv. */ + argv = reallocarray(argv, argc + 1, sizeof *argv); + if ((argv[argc++] = strdup(token)) == NULL) + errx(1, "strdup"); + } +out: + *ret_argv = argv; + *ret_argc = argc; + + return 0; +} + +void +argv_free(int argc, char **argv) +{ + int i; + + if (argc == 0) + return; + + for (i = 0; i < argc; i++) + free(argv[i]); + free(argv); +} + +static FILE * +find_mailcap_file(void) +{ + FILE *fp; + char **entry = (char **)default_mailcaps; + char *home = NULL; + char *expanded = NULL; + + for (; *entry != NULL; entry++) { + if (strncmp(*entry, "~/", 2) == 0) { + *entry += 2; + if ((home = getenv("HOME")) == NULL) + errx(1, "HOME not set"); + asprintf(&expanded, "%s/%s", home, *entry); + } else + asprintf(&expanded, "%s", *entry); + + fp = fopen(expanded, "r"); + free(expanded); + if (fp != NULL) + return (fp); + } + return (NULL); +} + +static struct mailcap * +mailcap_new(void) +{ + struct mailcap *mc = NULL; + + if ((mc = calloc(1, sizeof *mc)) == NULL) + errx(1, "calloc failed"); + + return (mc); +} + +static int +parse_mailcap_line(char *input) +{ + struct mailcap *mc; + int ms; + char *line = NULL; + + mc = mailcap_new(); + + for (ms = MAILCAP_MIME; ms < MAILCAP_END_OF_FIELDS; ms++) { + if ((line = strsep(&input, ";")) == NULL) + break; + + str_trim_whitespace(line); + + switch (ms) { + case MAILCAP_MIME: + if ((mc->mime_type = strdup(line)) == NULL) + errx(1, "strdup"); + break; + case MAILCAP_CMD: + if ((mc->cmd = strdup(line)) == NULL) + errx(1, "strdup"); + break; + case MAILCAP_FLAGS_1: + case MAILCAP_FLAGS_2: + if (strcmp(line, "needsterminal") == 0) + mc->flags |= MAILCAP_NEEDSTERMINAL; + if (strcmp(line, "copiousoutput") == 0) + mc->flags |= MAILCAP_COPIOUSOUTPUT; + break; + } + } + + if (line != NULL && *line != '\0') { + fprintf(stderr, "mailcap: trailing: %s: skipping...\n", line); + free(mc); + return (-1); + } + TAILQ_INSERT_TAIL(&mailcaps, mc, mailcaps); + + return (0); +} + +void +mailcap_expand_cmd(struct mailcap *mc, char *mt, char *file) +{ + char **argv; + int argc = 0, ret; + + if (mc->cmd == NULL) + return; + + ret = str_to_argv(mc->cmd, &argc, &argv); + + if (ret != 0 || argv == NULL) + return; + + for (int z = 0; z < argc; z++) { + if (strcmp(argv[z], "%s") == 0) { + free(argv[z]); + if ((argv[z] = strdup(file)) == NULL) + errx(1, "strdup"); + } + + if (strcmp(argv[z], "%t") == 0) { + free(argv[z]); + if ((argv[z] = strdup(mt)) == NULL) + errx(1, "strdup"); + } + } + argv[argc++] = NULL; + + argv_free(mc->cmd_argc, mc->cmd_argv); + + mc->cmd_argv = argv; + mc->cmd_argc = argc; +} + +static struct mailcap * +mailcap_by_mimetype(const char *mt) +{ + struct mailcap *mc; + + TAILQ_FOREACH(mc, &mailcaps, mailcaps) { + if (fnmatch(mc->mime_type, mt, 0) == 0) + return (mc); + } + return (NULL); +} + +void +init_mailcap(void) +{ + FILE *f = NULL; + const char delims[3] = {'\\', '\\', '\0'}; + char *buf, *copy; + size_t line = 0; + + if ((f = find_mailcap_file()) == NULL) + goto add_default; + + while ((buf = fparseln(f, NULL, &line, delims, 0)) != NULL) { + memset(&sps, 0, sizeof sps); + copy = buf; + + str_trim_whitespace(copy); + + if (*copy == '\0') { + free(buf); + continue; + } + + if (parse_mailcap_line(copy) == -1) { + fprintf(stderr, "Error with entry: <<%s>>, line: %ld\n", + copy, line); + } + free(copy); + } + fclose(f); + +add_default: + if ((copy = strdup(DEFAULT_MAILCAP_ENTRY)) == NULL) + errx(1, "strdup"); + + /* Our own entry won't error. */ + (void)parse_mailcap_line(copy); + free(copy); +} + +struct mailcap * +mailcap_cmd_from_mimetype(char *mime_type, char *filename) +{ + struct mailcap *mc = NULL; + + if (mime_type == NULL || filename == NULL) + return (NULL); + + if ((mc = mailcap_by_mimetype(mime_type)) != NULL) + mailcap_expand_cmd(mc, mime_type, filename); + + return (mc); +} blob - /dev/null blob + 442aa2d0d5c895e456d12ecac6ccd87f696f4544 (mode 644) --- /dev/null +++ mailcap.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Thomas Adam + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * 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 MAILCAP_H +#define MAILCAP_H + +#define DEFAULT_MIMETYPE "*/*" + +struct mailcap { + char *mime_type; + char *cmd; + char **cmd_argv; + int cmd_argc; + int flags; + + TAILQ_ENTRY(mailcap) mailcaps; +}; + +extern TAILQ_HEAD(mailcaplist, mailcap) mailcaps; + +extern struct mailcaplist mailcaps; + +void init_mailcap(void); +struct mailcap *mailcap_cmd_from_mimetype(char *, char *); + +#endif