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