libexec/dma: sync with upstream
[dragonfly.git] / usr.bin / calendar / io.c
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 *
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 *
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
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
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
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
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 *
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
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
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
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 *
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     
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     
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
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
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
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 }