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 } 727