Blob


1 /*
2 * Copyright (c) 2024 Thomas Adam <thomas@xteddy.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 /*
18 * Handles reading mailmap files.
19 */
21 #include "config.h"
23 #include <ctype.h>
24 #include <errno.h>
25 #include <fnmatch.h>
26 #include <limits.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
31 #include "compat.h"
32 #include "mailcap.h"
33 #include "xwrapper.h"
35 #define DEFAULT_MAILCAP_ENTRY "*/*; "DEFAULT_OPENER" %s"
37 #define str_unappend(ch) if (sps.off > 0 && (ch) != EOF) { sps.off--; }
39 struct str_parse_state {
40 char *buf;
41 size_t len, off;
42 int esc;
43 };
44 static struct str_parse_state sps;
46 static void str_append(char **, size_t *, char);
47 static int str_getc(void);
48 static char *str_tokenise(void);
49 static int str_to_argv(char *, int *, char ***);
50 static char *str_trim_whitespace(char *);
51 static void argv_free(int, char **);
53 struct mailcaplist mailcaps = TAILQ_HEAD_INITIALIZER(mailcaps);
55 static FILE *find_mailcap_file(void);
56 static struct mailcap *mailcap_new(void);
57 static void mailcap_expand_cmd(struct mailcap *, char *, char *);
58 static int parse_mailcap_line(char *);
59 static struct mailcap *mailcap_by_mimetype(const char *);
61 /* FIXME: $MAILCAPS can override this, but we don't check for that. */
62 static const char *default_mailcaps[] = {
63 "~/.mailcap",
64 "/etc/mailcap",
65 "/usr/etc/mailcap",
66 "/usr/local/etc/mailcap",
67 NULL,
68 };
70 enum mailcap_section {
71 MAILCAP_MIME = 0,
72 MAILCAP_CMD,
73 MAILCAP_FLAGS,
74 };
76 static char *
77 str_trim_whitespace(char *s)
78 {
79 char *t;
81 s += strspn(s, " \t\n");
82 t = s + strlen(s) - 1;
84 while (t >= s && isspace((unsigned char)*t))
85 *t-- = '\0';
86 return s;
87 }
89 static void
90 str_append(char **buf, size_t *len, char add)
91 {
92 size_t al = 1;
94 if (al > SIZE_MAX - 1 || *len > SIZE_MAX - 1 - al)
95 errx(1, "buffer is too big");
97 *buf = xrealloc(*buf, (*len) + 1 + al);
98 memcpy((*buf) + *len, &add, al);
99 (*len) += al;
102 static int
103 str_getc(void)
105 int ch;
107 if (sps.esc != 0) {
108 sps.esc--;
109 return ('\\');
111 for (;;) {
112 ch = EOF;
113 if (sps.off < sps.len)
114 ch = sps.buf[sps.off++];
116 if (ch == '\\') {
117 sps.esc++;
118 continue;
120 if (ch == '\n' && (sps.esc % 2) == 1) {
121 sps.esc--;
122 continue;
125 if (sps.esc != 0) {
126 str_unappend(ch);
127 sps.esc--;
128 return ('\\');
130 return (ch);
134 static char *
135 str_tokenise(void)
137 int ch;
138 char *buf;
139 size_t len;
140 enum {
141 APPEND,
142 DOUBLE_QUOTES,
143 SINGLE_QUOTES,
144 } state = APPEND;
146 len = 0;
147 buf = calloc(1, sizeof *buf);
149 for (;;) {
150 ch = str_getc();
151 /* EOF or \n are always the end of the token. */
152 if (ch == EOF || (state == APPEND && ch == '\n'))
153 break;
155 /* Whitespace ends a token unless inside quotes. But if we've
156 * also been given:
158 * foo "" bar
160 * don't lose that.
161 */
162 if (((ch == ' ' || ch == '\t') && state == APPEND) &&
163 buf[0] != '\0') {
164 break;
167 if (ch == '\\' && state != SINGLE_QUOTES) {
168 ch = str_getc();
169 str_append(&buf, &len, ch);
170 continue;
172 switch (ch) {
173 case '\'':
174 if (state == APPEND) {
175 state = SINGLE_QUOTES;
176 continue;
178 if (state == SINGLE_QUOTES) {
179 state = APPEND;
180 continue;
182 break;
183 case '"':
184 if (state == APPEND) {
185 state = DOUBLE_QUOTES;
186 continue;
188 if (state == DOUBLE_QUOTES) {
189 state = APPEND;
190 continue;
192 break;
193 default:
194 /* Otherwise add the character to the buffer. */
195 str_append(&buf, &len, ch);
196 break;
200 str_unappend(ch);
201 buf[len] = '\0';
203 if (*buf == '\0' || state == SINGLE_QUOTES || state == DOUBLE_QUOTES) {
204 fprintf(stderr, "Unterminated string: <%s>, missing: %c\n",
205 buf, state == SINGLE_QUOTES ? '\'' :
206 state == DOUBLE_QUOTES ? '"' : ' ');
207 free(buf);
208 return (NULL);
211 return (buf);
214 static int
215 str_to_argv(char *str, int *ret_argc, char ***ret_argv)
217 char *token;
218 int ch, next;
219 char **argv = NULL;
220 int argc = 0;
222 if (str == NULL)
223 return -1;
225 free(sps.buf);
226 memset(&sps, 0, sizeof(sps));
227 sps.buf = xstrdup(str);
228 sps.len = strlen(sps.buf);
230 for (;;) {
231 if (str[0] == '#') {
232 /* Skip comment. */
233 next = str_getc();
234 while (((next = str_getc()) != EOF))
235 ; /* Nothing. */
238 ch = str_getc();
240 if (ch == '\n' || ch == EOF)
241 goto out;
242 if (ch == ' ' || ch == '\t')
243 continue;
245 /* Tokenise the string according to quoting rules. Note that
246 * the string is stepped-back by one character to make the
247 * tokenisation easier, and not to kick-off the state of the
248 * parsing from this point.
249 */
250 str_unappend(ch);
251 if ((token = str_tokenise()) == NULL) {
252 argv_free(argc, argv);
253 return -1;
256 /* Add to argv. */
257 argv = xreallocarray(argv, argc + 1, sizeof *argv);
258 argv[argc++] = xstrdup(token);
260 out:
261 *ret_argv = argv;
262 *ret_argc = argc;
264 return 0;
267 void
268 argv_free(int argc, char **argv)
270 int i;
272 if (argc == 0)
273 return;
275 for (i = 0; i < argc; i++)
276 free(argv[i]);
277 free(argv);
280 static FILE *
281 find_mailcap_file(void)
283 FILE *fp;
284 char **entry = (char **)default_mailcaps;
285 char *home = NULL;
286 char *expanded = NULL;
288 for (; *entry != NULL; entry++) {
289 if (strncmp(*entry, "~/", 2) == 0) {
290 *entry += 2;
291 if ((home = getenv("HOME")) == NULL)
292 errx(1, "HOME not set");
293 asprintf(&expanded, "%s/%s", home, *entry);
294 } else
295 asprintf(&expanded, "%s", *entry);
297 fp = fopen(expanded, "r");
298 free(expanded);
299 if (fp != NULL)
300 return (fp);
302 return (NULL);
305 static struct mailcap *
306 mailcap_new(void)
308 struct mailcap *mc = NULL;
310 mc = xcalloc(1, sizeof *mc);
312 return (mc);
315 static int
316 parse_mailcap_line(char *input)
318 struct mailcap *mc;
319 int ms = 0;
320 char *line = NULL;
322 mc = mailcap_new();
324 while ((line = strsep(&input, ";")) != NULL) {
325 line = str_trim_whitespace(line);
327 switch (ms) {
328 case MAILCAP_MIME:
329 mc->mime_type = xstrdup(line);
330 ms++;
331 break;
332 case MAILCAP_CMD:
333 mc->cmd = xstrdup(line);
334 ms++;
335 break;
336 case MAILCAP_FLAGS:
337 if (strcmp(line, "needsterminal") == 0)
338 mc->flags |= MAILCAP_NEEDSTERMINAL;
339 if (strcmp(line, "copiousoutput") == 0)
340 mc->flags |= MAILCAP_COPIOUSOUTPUT;
341 break;
345 if (line != NULL && *line != '\0') {
346 fprintf(stderr, "mailcap: trailing: %s: skipping...\n", line);
347 free(mc);
348 return (-1);
350 TAILQ_INSERT_TAIL(&mailcaps, mc, mailcaps);
352 return (0);
355 void
356 mailcap_expand_cmd(struct mailcap *mc, char *mt, char *file)
358 char **argv;
359 int argc = 0, ret;
361 if (mc->cmd == NULL)
362 return;
364 ret = str_to_argv(mc->cmd, &argc, &argv);
366 if (ret != 0 || argv == NULL)
367 return;
369 for (int z = 0; z < argc; z++) {
370 if (strcmp(argv[z], "%s") == 0) {
371 free(argv[z]);
372 argv[z] = xstrdup(file);
375 if (strcmp(argv[z], "%t") == 0) {
376 free(argv[z]);
377 argv[z] = xstrdup(mt);
380 argv[argc++] = NULL;
382 argv_free(mc->cmd_argc, mc->cmd_argv);
384 mc->cmd_argv = argv;
385 mc->cmd_argc = argc;
388 static struct mailcap *
389 mailcap_by_mimetype(const char *mt)
391 struct mailcap *mc;
393 TAILQ_FOREACH(mc, &mailcaps, mailcaps) {
394 if (fnmatch(mc->mime_type, mt, 0) == 0)
395 return (mc);
397 return (NULL);
400 void
401 init_mailcap(void)
403 FILE *f;
404 char *copy;
406 if ((f = find_mailcap_file()) != NULL) {
407 mailcap_parse(f);
408 fclose(f);
411 copy = xstrdup(DEFAULT_MAILCAP_ENTRY);
413 /* Our own entry won't error. */
414 (void)parse_mailcap_line(copy);
415 free(copy);
418 void
419 mailcap_parse(FILE *f)
421 const char delims[3] = {'\\', '\\', '\0'};
422 char *buf, *copy;
423 size_t line = 0;
425 while ((buf = fparseln(f, NULL, &line, delims, 0)) != NULL) {
426 memset(&sps, 0, sizeof sps);
427 copy = buf;
429 copy = str_trim_whitespace(copy);
431 if (*copy == '#' || *copy == '\0') {
432 free(buf);
433 continue;
436 if (parse_mailcap_line(copy) == -1) {
437 fprintf(stderr, "Error with entry: <<%s>>, line: %ld\n",
438 copy, line);
440 free(buf);
444 struct mailcap *
445 mailcap_cmd_from_mimetype(char *mime_type, char *filename)
447 struct mailcap *mc = NULL;
449 if (mime_type == NULL || filename == NULL)
450 return (NULL);
452 if ((mc = mailcap_by_mimetype(mime_type)) != NULL)
453 mailcap_expand_cmd(mc, mime_type, filename);
455 return (mc);