xref: /dragonfly/usr.bin/calendar/io.c (revision d19ef5a2)
1 /*-
2  * SPDX-License-Identifier: BSD-3-Clause
3  *
4  * Copyright (c) 2020 The DragonFly Project.  All rights reserved.
5  * Copyright (c) 1989, 1993, 1994
6  *	The Regents of the University of California.  All rights reserved.
7  *
8  * This code is derived from software contributed to The DragonFly Project
9  * by Aaron LI <aly@aaronly.me>
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions
13  * are met:
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in the
18  *    documentation and/or other materials provided with the distribution.
19  * 3. Neither the name of the University nor the names of its contributors
20  *    may be used to endorse or promote products derived from this software
21  *    without specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
24  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
27  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33  * SUCH DAMAGE.
34  *
35  * @(#)calendar.c  8.3 (Berkeley) 3/25/94
36  * $FreeBSD: head/usr.bin/calendar/io.c 327117 2017-12-23 21:04:32Z eadler $
37  */
38 
39 #include <sys/param.h>
40 #include <sys/wait.h>
41 
42 #include <assert.h>
43 #include <ctype.h>
44 #include <err.h>
45 #include <langinfo.h>
46 #include <locale.h>
47 #include <paths.h>
48 #include <pwd.h>
49 #include <stdbool.h>
50 #include <stdio.h>
51 #include <stdlib.h>
52 #include <string.h>
53 #include <unistd.h>
54 
55 #include "calendar.h"
56 #include "basics.h"
57 #include "dates.h"
58 #include "days.h"
59 #include "gregorian.h"
60 #include "io.h"
61 #include "nnames.h"
62 #include "parsedata.h"
63 #include "utils.h"
64 
65 
66 enum { C_NONE, C_LINE, C_BLOCK };
67 enum { T_NONE, T_TOKEN, T_VARIABLE, T_DATE };
68 
69 struct cal_entry {
70 	int   type;		/* type of the read entry */
71 	char *token;		/* token to process (T_TOKEN) */
72 	char *variable;		/* variable name (T_VARIABLE) */
73 	char *value;		/* variable value (T_VARIABLE) */
74 	char *date;		/* event date (T_DATE) */
75 	struct cal_desc *description;  /* event description (T_DATE) */
76 };
77 
78 struct cal_file {
79 	FILE	*fp;
80 	char	*line;		/* line string read from file */
81 	size_t	 line_cap;	/* capacity of the 'line' buffer */
82 	char	*nextline;	/* to store the rewinded line */
83 	size_t	 nextline_cap;	/* capacity of the 'nextline' buffer */
84 	bool	 rewinded;	/* if 'nextline' has the rewinded line */
85 };
86 
87 static struct cal_desc *descriptions = NULL;
88 static struct node *definitions = NULL;
89 
90 static FILE	*cal_fopen(const char *file);
91 static bool	 cal_parse(FILE *in);
92 static bool	 process_token(char *line, bool *skip);
93 static void	 send_mail(FILE *fp);
94 static char	*skip_comment(char *line, int *comment);
95 static void	 write_mailheader(FILE *fp);
96 
97 static bool	 cal_readentry(struct cal_file *cfile,
98 			       struct cal_entry *entry, bool skip);
99 static char	*cal_readline(struct cal_file *cfile);
100 static void	 cal_rewindline(struct cal_file *cfile);
101 static bool	 is_date_entry(char *line, char **content);
102 static bool	 is_variable_entry(char *line, char **value);
103 
104 static struct cal_desc *cal_desc_new(struct cal_desc **head);
105 static void	 cal_desc_freeall(struct cal_desc *head);
106 static void	 cal_desc_addline(struct cal_desc *desc, const char *line);
107 
108 /*
109  * XXX: Quoted or escaped comment marks are not supported yet.
110  */
111 static char *
skip_comment(char * line,int * comment)112 skip_comment(char *line, int *comment)
113 {
114 	char *p, *pp;
115 
116 	if (*comment == C_LINE) {
117 		*line = '\0';
118 		*comment = C_NONE;
119 		return line;
120 	} else if (*comment == C_BLOCK) {
121 		for (p = line, pp = p + 1; *p; p++, pp = p + 1) {
122 			if (*p == '*' && *pp == '/') {
123 				*comment = C_NONE;
124 				return p + 2;
125 			}
126 		}
127 		*line = '\0';
128 		return line;
129 	} else {
130 		*comment = C_NONE;
131 		for (p = line, pp = p + 1; *p; p++, pp = p + 1) {
132 			if (*p == '/' && (*pp == '/' || *pp == '*')) {
133 				*comment = (*pp == '/') ? C_LINE : C_BLOCK;
134 				break;
135 			}
136 		}
137 		if (*comment != C_NONE) {
138 			pp = skip_comment(p, comment);
139 			if (pp > p)
140 				memmove(p, pp, strlen(pp) + 1);
141 		}
142 		return line;
143 	}
144 
145 	return line;
146 }
147 
148 
149 static FILE *
cal_fopen(const char * file)150 cal_fopen(const char *file)
151 {
152 	FILE *fp = NULL;
153 	char fpath[MAXPATHLEN];
154 
155 	for (size_t i = 0; calendarDirs[i] != NULL; i++) {
156 		snprintf(fpath, sizeof(fpath), "%s/%s",
157 			 calendarDirs[i], file);
158 		if ((fp = fopen(fpath, "r")) != NULL)
159 			return (fp);
160 	}
161 
162 	warnx("Cannot open calendar file: '%s'", file);
163 	return (NULL);
164 }
165 
166 /*
167  * NOTE: input 'line' should have trailing comment and whitespace trimmed.
168  */
169 static bool
process_token(char * line,bool * skip)170 process_token(char *line, bool *skip)
171 {
172 	char *walk;
173 
174 	if (strcmp(line, "#endif") == 0) {
175 		*skip = false;
176 		return true;
177 	}
178 
179 	if (*skip)  /* deal with nested #ifndef */
180 		return true;
181 
182 	if (string_startswith(line, "#include ") ||
183 	    string_startswith(line, "#include\t")) {
184 		walk = triml(line + sizeof("#include"));
185 		if (*walk == '\0') {
186 			warnx("Expecting arguments after #include");
187 			return false;
188 		}
189 		if (*walk != '<' && *walk != '\"') {
190 			warnx("Expecting '<' or '\"' after #include");
191 			return false;
192 		}
193 
194 		char a = *walk;
195 		char c = walk[strlen(walk) - 1];
196 
197 		switch(c) {
198 		case '>':
199 			if (a != '<') {
200 				warnx("Unterminated include expecting '\"'");
201 				return false;
202 			}
203 			break;
204 		case '\"':
205 			if (a != '\"') {
206 				warnx("Unterminated include expecting '>'");
207 				return false;
208 			}
209 			break;
210 		default:
211 			warnx("Unterminated include expecting '%c'",
212 			      (a == '<') ? '>' : '\"' );
213 			return false;
214 		}
215 
216 		walk++;
217 		walk[strlen(walk) - 1] = '\0';
218 
219 		FILE *fpin = cal_fopen(walk);
220 		if (fpin == NULL)
221 			return false;
222 		if (!cal_parse(fpin)) {
223 			warnx("Failed to parse calendar files");
224 			fclose(fpin);
225 			return false;
226 		}
227 
228 		fclose(fpin);
229 		return true;
230 
231 	} else if (string_startswith(line, "#define ") ||
232 	           string_startswith(line, "#define\t")) {
233 		walk = triml(line + sizeof("#define"));
234 		if (*walk == '\0') {
235 			warnx("Expecting arguments after #define");
236 			return false;
237 		}
238 
239 		struct node *new = list_newnode(xstrdup(walk), NULL);
240 		definitions = list_addfront(definitions, new);
241 
242 		return true;
243 
244 	} else if (string_startswith(line, "#ifndef ") ||
245 	           string_startswith(line, "#ifndef\t")) {
246 		walk = triml(line + sizeof("#ifndef"));
247 		if (*walk == '\0') {
248 			warnx("Expecting arguments after #ifndef");
249 			return false;
250 		}
251 
252 		if (list_lookup(definitions, walk, strcmp, NULL))
253 			*skip = true;
254 
255 		return true;
256 	}
257 
258 	warnx("Unknown token line: |%s|", line);
259 	return false;
260 }
261 
262 static bool
locale_day_first(void)263 locale_day_first(void)
264 {
265 	char *d_fmt = nl_langinfo(D_FMT);
266 	DPRINTF("%s: d_fmt=|%s|\n", __func__, d_fmt);
267 	/* NOTE: BSDs use '%e' in D_FMT while Linux uses '%d' */
268 	return (strpbrk(d_fmt, "ed") < strchr(d_fmt, 'm'));
269 }
270 
271 static bool
cal_parse(FILE * in)272 cal_parse(FILE *in)
273 {
274 	struct cal_file cfile = { 0 };
275 	struct cal_entry entry = { 0 };
276 	struct cal_desc *desc;
277 	struct cal_line *line;
278 	struct cal_day *cdays[CAL_MAX_REPEAT] = { NULL };
279 	struct specialday *sday;
280 	char *extradata[CAL_MAX_REPEAT] = { NULL };
281 	bool d_first, skip, var_handled;
282 	bool locale_changed, calendar_changed;
283 	int flags, count;
284 
285 	assert(in != NULL);
286 	cfile.fp = in;
287 	d_first = locale_day_first();
288 	skip = false;
289 	locale_changed = false;
290 	calendar_changed = false;
291 
292 	while (cal_readentry(&cfile, &entry, skip)) {
293 		if (entry.type == T_TOKEN) {
294 			DPRINTF2("%s: T_TOKEN: |%s|\n",
295 				 __func__, entry.token);
296 			if (!process_token(entry.token, &skip)) {
297 				free(entry.token);
298 				return false;
299 			}
300 
301 			free(entry.token);
302 			continue;
303 		}
304 
305 		if (entry.type == T_VARIABLE) {
306 			DPRINTF2("%s: T_VARIABLE: |%s|=|%s|\n",
307 				 __func__, entry.variable, entry.value);
308 			var_handled = false;
309 
310 			if (strcasecmp(entry.variable, "LANG") == 0) {
311 				if (setlocale(LC_ALL, entry.value) == NULL) {
312 					warnx("Failed to set LC_ALL='%s'",
313 					      entry.value);
314 				}
315 				d_first = locale_day_first();
316 				set_nnames();
317 				locale_changed = true;
318 				DPRINTF("%s: set LC_ALL='%s' (day_first=%s)\n",
319 					__func__, entry.value,
320 					d_first ? "true" : "false");
321 				var_handled = true;
322 			}
323 
324 			if (strcasecmp(entry.variable, "CALENDAR") == 0) {
325 				if (!set_calendar(entry.value)) {
326 					warnx("Failed to set CALENDAR='%s'",
327 					      entry.value);
328 				}
329 				calendar_changed = true;
330 				DPRINTF("%s: set CALENDAR='%s'\n",
331 					__func__, entry.value);
332 				var_handled = true;
333 			}
334 
335 			if (strcasecmp(entry.variable, "SEQUENCE") == 0) {
336 				set_nsequences(entry.value);
337 				var_handled = true;
338 			}
339 
340 			for (size_t i = 0; specialdays[i].name; i++) {
341 				sday = &specialdays[i];
342 				if (strcasecmp(entry.variable, sday->name) == 0) {
343 					free(sday->n_name);
344 					sday->n_name = xstrdup(entry.value);
345 					sday->n_len = strlen(sday->n_name);
346 					var_handled = true;
347 					break;
348 				}
349 			}
350 
351 			if (!var_handled) {
352 				warnx("Unknown variable: |%s|=|%s|",
353 				      entry.variable, entry.value);
354 			}
355 
356 			free(entry.variable);
357 			free(entry.value);
358 			continue;
359 		}
360 
361 		if (entry.type == T_DATE) {
362 			desc = entry.description;
363 			DPRINTF2("----------------\n%s: T_DATE: |%s|\n",
364 				 __func__, entry.date);
365 			for (line = desc->firstline; line; line = line->next)
366 				DPRINTF3("\t|%s|\n", line->str);
367 
368 			count = parse_cal_date(entry.date, &flags, cdays,
369 					       extradata);
370 			if (count < 0) {
371 				warnx("Cannot parse date |%s| with content |%s|",
372 				      entry.date, desc->firstline->str);
373 				continue;
374 			} else if (count == 0) {
375 				DPRINTF2("Ignore out-of-range date |%s| "
376 					 "with content |%s|\n",
377 					 entry.date, desc->firstline->str);
378 				continue;
379 			}
380 
381 			for (int i = 0; i < count; i++) {
382 				event_add(cdays[i], d_first,
383 				          ((flags & F_VARIABLE) != 0),
384 				          desc, extradata[i]);
385 				cdays[i] = NULL;
386 				extradata[i] = NULL;
387 			}
388 
389 			free(entry.date);
390 			continue;
391 		}
392 
393 		errx(1, "Invalid calendar entry type: %d", entry.type);
394 	}
395 
396 	/*
397 	 * Reset to the default locale, so that one calendar file that changed
398 	 * the locale (by defining the "LANG" variable) does not interfere the
399 	 * following calendar files without the "LANG" definition.
400 	 */
401 	if (locale_changed) {
402 		setlocale(LC_ALL, "");
403 		set_nnames();
404 		DPRINTF("%s: reset LC_ALL\n", __func__);
405 	}
406 
407 	if (calendar_changed) {
408 		set_calendar(NULL);
409 		DPRINTF("%s: reset CALENDAR\n", __func__);
410 	}
411 
412 	free(cfile.line);
413 	free(cfile.nextline);
414 
415 	return true;
416 }
417 
418 static bool
cal_readentry(struct cal_file * cfile,struct cal_entry * entry,bool skip)419 cal_readentry(struct cal_file *cfile, struct cal_entry *entry, bool skip)
420 {
421 	char *p, *value, *content;
422 	int comment;
423 
424 	memset(entry, 0, sizeof(*entry));
425 	entry->type = T_NONE;
426 	comment = C_NONE;
427 
428 	while ((p = cal_readline(cfile)) != NULL) {
429 		p = skip_comment(p, &comment);
430 		p = trimr(p);  /* Need to keep the leading tabs */
431 		if (*p == '\0')
432 			continue;
433 
434 		if (*p == '#') {
435 			entry->type = T_TOKEN;
436 			entry->token = xstrdup(p);
437 			return true;
438 		}
439 
440 		if (skip) {
441 			/* skip entries but tokens (e.g., '#endif') */
442 			DPRINTF2("%s: skip line: |%s|\n", __func__, p);
443 			continue;
444 		}
445 
446 		if (is_variable_entry(p, &value)) {
447 			value = triml(value);
448 			if (*value == '\0') {
449 				warnx("%s: varaible |%s| has no value",
450 				      __func__, p);
451 				continue;
452 			}
453 
454 			entry->type = T_VARIABLE;
455 			entry->variable = xstrdup(p);
456 			entry->value = xstrdup(value);
457 			return true;
458 		}
459 
460 		if (is_date_entry(p, &content)) {
461 			content = triml(content);
462 			if (*content == '\0') {
463 				warnx("%s: date |%s| has no content",
464 				      __func__, p);
465 				continue;
466 			}
467 
468 			entry->type = T_DATE;
469 			entry->date = xstrdup(p);
470 			entry->description = cal_desc_new(&descriptions);
471 			cal_desc_addline(entry->description, content);
472 
473 			/* Continuous description of the event */
474 			while ((p = cal_readline(cfile)) != NULL) {
475 				p = trimr(skip_comment(p, &comment));
476 				if (*p == '\0')
477 					continue;
478 
479 				if (*p == '\t') {
480 					content = triml(p);
481 					cal_desc_addline(entry->description,
482 							 content);
483 				} else {
484 					cal_rewindline(cfile);
485 					break;
486 				}
487 			}
488 
489 			return true;
490 		}
491 
492 		warnx("%s: unknown line: |%s|", __func__, p);
493 	}
494 
495 	return false;
496 }
497 
498 static char *
cal_readline(struct cal_file * cfile)499 cal_readline(struct cal_file *cfile)
500 {
501 	if (cfile->rewinded) {
502 		cfile->rewinded = false;
503 		return cfile->nextline;
504 	}
505 
506 	if (getline(&cfile->line, &cfile->line_cap, cfile->fp) <= 0)
507 		return NULL;
508 
509 	return cfile->line;
510 }
511 
512 static void
cal_rewindline(struct cal_file * cfile)513 cal_rewindline(struct cal_file *cfile)
514 {
515 	if (cfile->nextline_cap == 0)
516 		cfile->nextline = xmalloc(cfile->line_cap);
517 	else if (cfile->nextline_cap < cfile->line_cap)
518 		cfile->nextline = xrealloc(cfile->nextline, cfile->line_cap);
519 
520 	memcpy(cfile->nextline, cfile->line, cfile->line_cap);
521 	cfile->nextline_cap = cfile->line_cap;
522 	cfile->rewinded = true;
523 }
524 
525 static bool
is_variable_entry(char * line,char ** value)526 is_variable_entry(char *line, char **value)
527 {
528 	char *p, *eq;
529 
530 	if (line == NULL)
531 		return false;
532 	if (!(*line == '_' || isalpha((unsigned int)*line)))
533 		return false;
534 	if ((eq = strchr(line, '=')) == NULL)
535 		return false;
536 	for (p = line+1; p < eq; p++) {
537 		if (!isalnum((unsigned int)*p))
538 			return false;
539 	}
540 
541 	*eq = '\0';
542 	if (value != NULL)
543 		*value = eq + 1;
544 
545 	return true;
546 }
547 
548 static bool
is_date_entry(char * line,char ** content)549 is_date_entry(char *line, char **content)
550 {
551 	char *p;
552 
553 	if (*line == '\t')
554 		return false;
555 	if ((p = strchr(line, '\t')) == NULL)
556 		return false;
557 
558 	*p = '\0';
559 	if (content != NULL)
560 		*content = p + 1;
561 
562 	return true;
563 }
564 
565 
566 static struct cal_desc *
cal_desc_new(struct cal_desc ** head)567 cal_desc_new(struct cal_desc **head)
568 {
569 	struct cal_desc *desc = xcalloc(1, sizeof(*desc));
570 
571 	if (*head == NULL) {
572 		*head = desc;
573 	} else {
574 		desc->next = *head;
575 		*head = desc;
576 	}
577 
578 	return desc;
579 }
580 
581 static void
cal_desc_freeall(struct cal_desc * head)582 cal_desc_freeall(struct cal_desc *head)
583 {
584 	struct cal_desc *desc;
585 	struct cal_line *line;
586 
587 	while ((desc = head) != NULL) {
588 		head = head->next;
589 		while ((line = desc->firstline) != NULL) {
590 			desc->firstline = desc->firstline->next;
591 			free(line->str);
592 			free(line);
593 		}
594 		free(desc);
595 	}
596 }
597 
598 static void
cal_desc_addline(struct cal_desc * desc,const char * line)599 cal_desc_addline(struct cal_desc *desc, const char *line)
600 {
601 	struct cal_line *cline;
602 
603 	cline = xcalloc(1, sizeof(*cline));
604 	cline->str = xstrdup(line);
605 	if (desc->lastline != NULL) {
606 		desc->lastline->next = cline;
607 		desc->lastline = cline;
608 	} else {
609 		desc->firstline = desc->lastline = cline;
610 	}
611 }
612 
613 
614 int
cal(FILE * fpin)615 cal(FILE *fpin)
616 {
617 	if (!cal_parse(fpin)) {
618 		warnx("Failed to parse calendar files");
619 		return 1;
620 	}
621 
622 	if (Options.allmode) {
623 		FILE *fpout;
624 
625 		/*
626 		 * Use a temporary output file, so we can skip sending mail
627 		 * if there is no output.
628 		 */
629 		if ((fpout = tmpfile()) == NULL) {
630 			warn("tmpfile");
631 			return 1;
632 		}
633 		event_print_all(fpout);
634 		send_mail(fpout);
635 	} else {
636 		event_print_all(stdout);
637 	}
638 
639 	list_freeall(definitions, free, NULL);
640 	definitions = NULL;
641 	cal_desc_freeall(descriptions);
642 	descriptions = NULL;
643 
644 	return 0;
645 }
646 
647 
648 static void
send_mail(FILE * fp)649 send_mail(FILE *fp)
650 {
651 	int ch, pdes[2];
652 	FILE *fpipe;
653 
654 	assert(Options.allmode == true);
655 
656 	if (fseek(fp, 0L, SEEK_END) == -1 || ftell(fp) == 0) {
657 		DPRINTF("%s: no events; skip sending mail\n", __func__);
658 		return;
659 	}
660 	if (pipe(pdes) < 0) {
661 		warnx("pipe");
662 		return;
663 	}
664 
665 	switch (fork()) {
666 	case -1:
667 		close(pdes[0]);
668 		close(pdes[1]);
669 		goto done;
670 	case 0:
671 		/* child -- set stdin to pipe output */
672 		if (pdes[0] != STDIN_FILENO) {
673 			dup2(pdes[0], STDIN_FILENO);
674 			close(pdes[0]);
675 		}
676 		close(pdes[1]);
677 		execl(_PATH_SENDMAIL, "sendmail", "-i", "-t", "-F",
678 		      "\"Reminder Service\"", (char *)NULL);
679 		warn(_PATH_SENDMAIL);
680 		_exit(1);
681 	}
682 	/* parent -- write to pipe input */
683 	close(pdes[0]);
684 
685 	fpipe = fdopen(pdes[1], "w");
686 	if (fpipe == NULL) {
687 		close(pdes[1]);
688 		goto done;
689 	}
690 
691 	write_mailheader(fpipe);
692 	rewind(fp);
693 	while ((ch = fgetc(fp)) != EOF)
694 		fputc(ch, fpipe);
695 	fclose(fpipe);  /* will also close the underlying fd */
696 
697 done:
698 	fclose(fp);
699 	while (wait(NULL) >= 0)
700 		;
701 }
702 
703 static void
write_mailheader(FILE * fp)704 write_mailheader(FILE *fp)
705 {
706 	uid_t uid = getuid();
707 	struct passwd *pw = getpwuid(uid);
708 	struct date date;
709 	char dayname[32] = { 0 };
710 	int dow;
711 
712 	gregorian_from_fixed(Options.today, &date);
713 	dow = dayofweek_from_fixed(Options.today);
714 	sprintf(dayname, "%s, %d %s %d",
715 		dow_names[dow].f_name, date.day,
716 		month_names[date.month-1].f_name, date.year);
717 
718 	fprintf(fp,
719 		"From: %s (Reminder Service)\n"
720 		"To: %s\n"
721 		"Subject: %s's Calendar\n"
722 		"Precedence: bulk\n"
723 		"Auto-Submitted: auto-generated\n\n",
724 		pw->pw_name, pw->pw_name, dayname);
725 	fflush(fp);
726 }
727