1 /*- 2 * ------+---------+---------+---------+---------+---------+---------+---------* 3 * Initial version of parse8601 was originally added to newsyslog.c in 4 * FreeBSD on Jan 22, 1999 by Garrett Wollman <wollman@FreeBSD.org>. 5 * Initial version of parseDWM was originally added to newsyslog.c in 6 * FreeBSD on Apr 4, 2000 by Hellmuth Michaelis <hm@FreeBSD.org>. 7 * 8 * Copyright (c) 2003 - Garance Alistair Drosehn <gad@FreeBSD.org>. 9 * All rights reserved. 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 * 20 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 21 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 24 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 26 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 28 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 29 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 30 * SUCH DAMAGE. 31 * 32 * The views and conclusions contained in the software and documentation 33 * are those of the authors and should not be interpreted as representing 34 * official policies, either expressed or implied, of the FreeBSD Project. 35 * 36 * ------+---------+---------+---------+---------+---------+---------+---------* 37 * This is intended to be a set of general-purpose routines to process times. 38 * Right now it probably still has a number of assumptions in it, such that 39 * it works fine for newsyslog but might not work for other uses. 40 * ------+---------+---------+---------+---------+---------+---------+---------* 41 * 42 * $FreeBSD: head/usr.sbin/newsyslog/ptimes.c 318960 2017-05-26 16:36:30Z dab $ 43 */ 44 45 #include <ctype.h> 46 #include <limits.h> 47 #include <stdio.h> 48 #include <stdlib.h> 49 #include <string.h> 50 #include <time.h> 51 52 #include "extern.h" 53 54 #define SECS_PER_HOUR 3600 55 56 /* 57 * Bit-values which indicate which components of time were specified 58 * by the string given to parse8601 or parseDWM. These are needed to 59 * calculate what time-in-the-future will match that string. 60 */ 61 #define TSPEC_YEAR 0x0001 62 #define TSPEC_MONTHOFYEAR 0x0002 63 #define TSPEC_LDAYOFMONTH 0x0004 64 #define TSPEC_DAYOFMONTH 0x0008 65 #define TSPEC_DAYOFWEEK 0x0010 66 #define TSPEC_HOUROFDAY 0x0020 67 68 #define TNYET_ADJ4DST -10 /* DST has "not yet" been adjusted */ 69 70 struct ptime_data { 71 time_t basesecs; /* Base point for relative times */ 72 time_t tsecs; /* Time in seconds */ 73 struct tm basetm; /* Base Time expanded into fields */ 74 struct tm tm; /* Time expanded into fields */ 75 int did_adj4dst; /* Track calls to ptime_adjust4dst */ 76 int parseopts; /* Options given for parsing */ 77 int tmspec; /* Indicates which time fields had 78 * been specified by the user */ 79 }; 80 81 static int days_pmonth(int month, int year); 82 static int parse8601(struct ptime_data *ptime, const char *str); 83 static int parseDWM(struct ptime_data *ptime, const char *str); 84 85 /* 86 * Simple routine to calculate the number of days in a given month. 87 */ 88 static int 89 days_pmonth(int month, int year) 90 { 91 static const int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31, 92 30, 31, 30, 31}; 93 int ndays; 94 95 ndays = mtab[month]; 96 97 if (month == 1) { 98 /* 99 * We are usually called with a 'tm-year' value 100 * (ie, the value = the number of years past 1900). 101 */ 102 if (year < 1900) 103 year += 1900; 104 if (year % 4 == 0) { 105 /* 106 * This is a leap year, as long as it is not a 107 * multiple of 100, or if it is a multiple of 108 * both 100 and 400. 109 */ 110 if (year % 100 != 0) 111 ndays++; /* not multiple of 100 */ 112 else if (year % 400 == 0) 113 ndays++; /* is multiple of 100 and 400 */ 114 } 115 } 116 return (ndays); 117 } 118 119 /*- 120 * Parse a limited subset of ISO 8601. The specific format is as follows: 121 * 122 * [CC[YY[MM[DD]]]][THH[MM[SS]]] (where `T' is the literal letter) 123 * 124 * We don't accept a timezone specification; missing fields (including timezone) 125 * are defaulted to the current date but time zero. 126 */ 127 static int 128 parse8601(struct ptime_data *ptime, const char *s) 129 { 130 char *t; 131 long l; 132 struct tm tm; 133 134 l = strtol(s, &t, 10); 135 if (l < 0 || l >= INT_MAX || (*t != '\0' && *t != 'T')) 136 return (-1); 137 138 /* 139 * Now t points either to the end of the string (if no time was 140 * provided) or to the letter `T' which separates date and time in 141 * ISO 8601. The pointer arithmetic is the same for either case. 142 */ 143 tm = ptime->tm; 144 ptime->tmspec = TSPEC_HOUROFDAY; 145 switch (t - s) { 146 case 8: 147 tm.tm_year = ((l / 1000000) - 19) * 100; 148 l = l % 1000000; 149 /* FALLTHROUGH */ 150 case 6: 151 ptime->tmspec |= TSPEC_YEAR; 152 tm.tm_year -= tm.tm_year % 100; 153 tm.tm_year += l / 10000; 154 l = l % 10000; 155 /* FALLTHROUGH */ 156 case 4: 157 ptime->tmspec |= TSPEC_MONTHOFYEAR; 158 tm.tm_mon = (l / 100) - 1; 159 l = l % 100; 160 /* FALLTHROUGH */ 161 case 2: 162 ptime->tmspec |= TSPEC_DAYOFMONTH; 163 tm.tm_mday = l; 164 /* FALLTHROUGH */ 165 case 0: 166 break; 167 default: 168 return (-1); 169 } 170 171 /* sanity check */ 172 if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12 173 || tm.tm_mday < 1 || tm.tm_mday > 31) 174 return (-1); 175 176 if (*t != '\0') { 177 s = ++t; 178 l = strtol(s, &t, 10); 179 if (l < 0 || l >= INT_MAX || (*t != '\0' && !isspace(*t))) 180 return (-1); 181 182 switch (t - s) { 183 case 6: 184 tm.tm_sec = l % 100; 185 l /= 100; 186 /* FALLTHROUGH */ 187 case 4: 188 tm.tm_min = l % 100; 189 l /= 100; 190 /* FALLTHROUGH */ 191 case 2: 192 ptime->tmspec |= TSPEC_HOUROFDAY; 193 tm.tm_hour = l; 194 case 0: 195 break; 196 default: 197 return (-1); 198 } 199 200 /* sanity check */ 201 if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0 202 || tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23) 203 return (-1); 204 } 205 206 ptime->tm = tm; 207 return (0); 208 } 209 210 /*- 211 * Parse a cyclic time specification, the format is as follows: 212 * 213 * [Dhh] or [Wd[Dhh]] or [Mdd[Dhh]] 214 * 215 * to rotate a logfile cyclic at 216 * 217 * - every day (D) within a specific hour (hh) (hh = 0...23) 218 * - once a week (W) at a specific day (d) OR (d = 0..6, 0 = Sunday) 219 * - once a month (M) at a specific day (d) (d = 1..31,l|L) 220 * 221 * We don't accept a timezone specification; missing fields 222 * are defaulted to the current date but time zero. 223 */ 224 static int 225 parseDWM(struct ptime_data *ptime, const char *s) 226 { 227 int daysmon, Dseen, WMseen; 228 const char *endval; 229 char *tmp; 230 long l; 231 struct tm tm; 232 233 /* Save away the number of days in this month */ 234 tm = ptime->tm; 235 daysmon = days_pmonth(tm.tm_mon, tm.tm_year); 236 237 WMseen = Dseen = 0; 238 ptime->tmspec = TSPEC_HOUROFDAY; 239 for (;;) { 240 endval = NULL; 241 switch (*s) { 242 case 'D': 243 if (Dseen) 244 return (-1); 245 Dseen++; 246 ptime->tmspec |= TSPEC_HOUROFDAY; 247 s++; 248 l = strtol(s, &tmp, 10); 249 if (l < 0 || l > 23) 250 return (-1); 251 endval = tmp; 252 tm.tm_hour = l; 253 break; 254 255 case 'W': 256 if (WMseen) 257 return (-1); 258 WMseen++; 259 ptime->tmspec |= TSPEC_DAYOFWEEK; 260 s++; 261 l = strtol(s, &tmp, 10); 262 if (l < 0 || l > 6) 263 return (-1); 264 endval = tmp; 265 if (l != tm.tm_wday) { 266 int save; 267 268 if (l < tm.tm_wday) { 269 save = 6 - tm.tm_wday; 270 save += (l + 1); 271 } else { 272 save = l - tm.tm_wday; 273 } 274 275 tm.tm_mday += save; 276 277 if (tm.tm_mday > daysmon) { 278 tm.tm_mon++; 279 tm.tm_mday = tm.tm_mday - daysmon; 280 } 281 } 282 break; 283 284 case 'M': 285 if (WMseen) 286 return (-1); 287 WMseen++; 288 ptime->tmspec |= TSPEC_DAYOFMONTH; 289 s++; 290 if (tolower(*s) == 'l') { 291 /* User wants the last day of the month. */ 292 ptime->tmspec |= TSPEC_LDAYOFMONTH; 293 tm.tm_mday = daysmon; 294 endval = s + 1; 295 } else { 296 l = strtol(s, &tmp, 10); 297 if (l < 1 || l > 31) 298 return (-1); 299 300 if (l > daysmon) 301 return (-1); 302 endval = tmp; 303 tm.tm_mday = l; 304 } 305 break; 306 307 default: 308 return (-1); 309 break; 310 } 311 312 if (endval == NULL) 313 return (-1); 314 else if (*endval == '\0' || isspace(*endval)) 315 break; 316 else 317 s = endval; 318 } 319 320 ptime->tm = tm; 321 return (0); 322 } 323 324 /* 325 * Initialize a new ptime-related data area. 326 */ 327 struct ptime_data * 328 ptime_init(const struct ptime_data *optsrc) 329 { 330 struct ptime_data *newdata; 331 332 newdata = malloc(sizeof(struct ptime_data)); 333 if (optsrc != NULL) { 334 memcpy(newdata, optsrc, sizeof(struct ptime_data)); 335 } else { 336 memset(newdata, '\0', sizeof(struct ptime_data)); 337 newdata->did_adj4dst = TNYET_ADJ4DST; 338 } 339 340 return (newdata); 341 } 342 343 /* 344 * Adjust a given time if that time is in a different timezone than 345 * some other time. 346 */ 347 int 348 ptime_adjust4dst(struct ptime_data *ptime, const struct ptime_data *dstsrc) 349 { 350 struct ptime_data adjtime; 351 352 if (ptime == NULL) 353 return (-1); 354 355 /* 356 * Changes are not made to the given time until after all 357 * of the calculations have been successful. 358 */ 359 adjtime = *ptime; 360 361 /* Check to see if this adjustment was already made */ 362 if ((adjtime.did_adj4dst != TNYET_ADJ4DST) && 363 (adjtime.did_adj4dst == dstsrc->tm.tm_isdst)) 364 return (0); /* yes, so don't make it twice */ 365 366 /* See if daylight-saving has changed between the two times. */ 367 if (dstsrc->tm.tm_isdst != adjtime.tm.tm_isdst) { 368 if (adjtime.tm.tm_isdst == 1) 369 adjtime.tsecs -= SECS_PER_HOUR; 370 else if (adjtime.tm.tm_isdst == 0) 371 adjtime.tsecs += SECS_PER_HOUR; 372 adjtime.tm = *(localtime(&adjtime.tsecs)); 373 /* Remember that this adjustment has been made */ 374 adjtime.did_adj4dst = dstsrc->tm.tm_isdst; 375 /* 376 * XXX - Should probably check to see if changing the 377 * hour also changed the value of is_dst. What 378 * should we do in that case? 379 */ 380 } 381 382 *ptime = adjtime; 383 return (0); 384 } 385 386 int 387 ptime_relparse(struct ptime_data *ptime, int parseopts, time_t basetime, 388 const char *str) 389 { 390 int dpm, pres; 391 struct tm temp_tm; 392 393 ptime->parseopts = parseopts; 394 ptime->basesecs = basetime; 395 ptime->basetm = *(localtime(&ptime->basesecs)); 396 ptime->tm = ptime->basetm; 397 ptime->tm.tm_hour = ptime->tm.tm_min = ptime->tm.tm_sec = 0; 398 399 /* 400 * Call a routine which sets ptime.tm and ptime.tspecs based 401 * on the given string and parsing-options. Note that the 402 * routine should not call mktime to set ptime.tsecs. 403 */ 404 if (parseopts & PTM_PARSE_DWM) 405 pres = parseDWM(ptime, str); 406 else 407 pres = parse8601(ptime, str); 408 if (pres < 0) { 409 ptime->tsecs = (time_t)pres; 410 return (pres); 411 } 412 413 /* 414 * Before calling mktime, check to see if we ended up with a 415 * "day-of-month" that does not exist in the selected month. 416 * If we did call mktime with that info, then mktime will 417 * make it look like the user specifically requested a day 418 * in the following month (eg: Feb 31 turns into Mar 3rd). 419 */ 420 dpm = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); 421 if ((parseopts & PTM_PARSE_MATCHDOM) && 422 (ptime->tmspec & TSPEC_DAYOFMONTH) && 423 (ptime->tm.tm_mday> dpm)) { 424 /* 425 * ptime_nxtime() will want a ptime->tsecs value, 426 * but we need to avoid mktime resetting all the 427 * ptime->tm values. 428 */ 429 if (verbose && dbg_at_times > 1) 430 fprintf(stderr, 431 "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)", 432 ptime->tm.tm_year, ptime->tm.tm_mon, 433 ptime->tm.tm_mday, ptime->tm.tm_hour, 434 ptime->tm.tm_min, dpm); 435 temp_tm = ptime->tm; 436 ptime->tsecs = mktime(&temp_tm); 437 if (ptime->tsecs > (time_t)-1) 438 ptimeset_nxtime(ptime); 439 if (verbose && dbg_at_times > 1) 440 fprintf(stderr, 441 " to: %4d/%02d/%02d %02d:%02d\n", 442 ptime->tm.tm_year, ptime->tm.tm_mon, 443 ptime->tm.tm_mday, ptime->tm.tm_hour, 444 ptime->tm.tm_min); 445 } 446 447 /* 448 * Convert the ptime.tm into standard time_t seconds. Check 449 * for invalid times, which includes things like the hour lost 450 * when switching from "standard time" to "daylight saving". 451 */ 452 ptime->tsecs = mktime(&ptime->tm); 453 if (ptime->tsecs == (time_t)-1) { 454 ptime->tsecs = (time_t)-2; 455 return (-2); 456 } 457 458 return (0); 459 } 460 461 int 462 ptime_free(struct ptime_data *ptime) 463 { 464 465 if (ptime == NULL) 466 return (-1); 467 468 free(ptime); 469 return (0); 470 } 471 472 /* 473 * Some trivial routines so ptime_data can remain a completely 474 * opaque type. 475 */ 476 const char * 477 ptimeget_ctime(const struct ptime_data *ptime) 478 { 479 480 if (ptime == NULL) 481 return ("Null time in ptimeget_ctime()\n"); 482 483 return (ctime(&ptime->tsecs)); 484 } 485 486 /* 487 * Generate a time of day string in an RFC5424 compatible format. Return a 488 * pointer to the buffer with the timestamp string or NULL if an error. If the 489 * time is not supplied, cannot be converted to local time, or the resulting 490 * string would overflow the buffer, the returned string will be the RFC5424 491 * NILVALUE. 492 */ 493 char * 494 ptimeget_ctime_rfc5424(const struct ptime_data *ptime, 495 char *timebuf, size_t bufsize) 496 { 497 static const char NILVALUE[] = {"-"}; /* RFC5424 specified NILVALUE */ 498 int chars; 499 struct tm tm; 500 int tz_hours; 501 int tz_mins; 502 long tz_offset; 503 char tz_sign; 504 505 if (timebuf == NULL) { 506 return (NULL); 507 } 508 509 if (bufsize < sizeof(NILVALUE)) { 510 return (NULL); 511 } 512 513 /* 514 * Convert to localtime. RFC5424 mandates the use of the NILVALUE if 515 * the time cannot be obtained, so use that if there is an error in the 516 * conversion. 517 */ 518 if (ptime == NULL || localtime_r(&(ptime->tsecs), &tm) == NULL) { 519 strlcpy(timebuf, NILVALUE, bufsize); 520 return (timebuf); 521 } 522 523 /* 524 * Convert the time to a string in RFC5424 format. The conversion 525 * cannot be done with strftime() because it cannot produce the correct 526 * timezone offset format. 527 */ 528 if (tm.tm_gmtoff < 0) { 529 tz_sign = '-'; 530 tz_offset = -tm.tm_gmtoff; 531 } else { 532 tz_sign = '+'; 533 tz_offset = tm.tm_gmtoff; 534 } 535 536 tz_hours = tz_offset / 3600; 537 tz_mins = (tz_offset % 3600) / 60; 538 539 chars = snprintf(timebuf, bufsize, 540 "%04d-%02d-%02d" /* date */ 541 "T%02d:%02d:%02d" /* time */ 542 "%c%02d:%02d", /* time zone offset */ 543 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, 544 tm.tm_hour, tm.tm_min, tm.tm_sec, 545 tz_sign, tz_hours, tz_mins); 546 547 /* If the timestamp is too big for timebuf, return the NILVALUE. */ 548 if (chars >= (int)bufsize) { 549 strlcpy(timebuf, NILVALUE, bufsize); 550 } 551 552 return (timebuf); 553 } 554 555 double 556 ptimeget_diff(const struct ptime_data *minuend, const struct 557 ptime_data *subtrahend) 558 { 559 560 /* Just like difftime(), we have no good error-return */ 561 if (minuend == NULL || subtrahend == NULL) 562 return (0.0); 563 564 return (difftime(minuend->tsecs, subtrahend->tsecs)); 565 } 566 567 time_t 568 ptimeget_secs(const struct ptime_data *ptime) 569 { 570 571 if (ptime == NULL) 572 return (-1); 573 574 return (ptime->tsecs); 575 } 576 577 /* 578 * Generate an approximate timestamp for the next event, based on 579 * what parts of time were specified by the original parameter to 580 * ptime_relparse(). The result may be -1 if there is no obvious 581 * "next time" which will work. 582 */ 583 int 584 ptimeset_nxtime(struct ptime_data *ptime) 585 { 586 int moredays, tdpm, tmon, tyear; 587 struct ptime_data nextmatch; 588 589 if (ptime == NULL) 590 return (-1); 591 592 /* 593 * Changes are not made to the given time until after all 594 * of the calculations have been successful. 595 */ 596 nextmatch = *ptime; 597 /* 598 * If the user specified a year and we're already past that 599 * time, then there will never be another one! 600 */ 601 if (ptime->tmspec & TSPEC_YEAR) 602 return (-1); 603 604 /* 605 * The caller gave us a time in the past. Calculate how much 606 * time is needed to go from that valid rotate time to the 607 * next valid rotate time. We only need to get to the nearest 608 * hour, because newsyslog is only run once per hour. 609 */ 610 moredays = 0; 611 if (ptime->tmspec & TSPEC_MONTHOFYEAR) { 612 /* Special case: Feb 29th does not happen every year. */ 613 if (ptime->tm.tm_mon == 1 && ptime->tm.tm_mday == 29) { 614 nextmatch.tm.tm_year += 4; 615 if (days_pmonth(1, nextmatch.tm.tm_year) < 29) 616 nextmatch.tm.tm_year += 4; 617 } else { 618 nextmatch.tm.tm_year += 1; 619 } 620 nextmatch.tm.tm_isdst = -1; 621 nextmatch.tsecs = mktime(&nextmatch.tm); 622 623 } else if (ptime->tmspec & TSPEC_LDAYOFMONTH) { 624 /* 625 * Need to get to the last day of next month. Origtm is 626 * already at the last day of this month, so just add to 627 * it number of days in the next month. 628 */ 629 if (ptime->tm.tm_mon < 11) 630 moredays = days_pmonth(ptime->tm.tm_mon + 1, 631 ptime->tm.tm_year); 632 else 633 moredays = days_pmonth(0, ptime->tm.tm_year + 1); 634 635 } else if (ptime->tmspec & TSPEC_DAYOFMONTH) { 636 /* Jump to the same day in the next month */ 637 moredays = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); 638 /* 639 * In some cases, the next month may not *have* the 640 * desired day-of-the-month. If that happens, then 641 * move to the next month that does have enough days. 642 */ 643 tmon = ptime->tm.tm_mon; 644 tyear = ptime->tm.tm_year; 645 for (;;) { 646 if (tmon < 11) 647 tmon += 1; 648 else { 649 tmon = 0; 650 tyear += 1; 651 } 652 tdpm = days_pmonth(tmon, tyear); 653 if (tdpm >= ptime->tm.tm_mday) 654 break; 655 moredays += tdpm; 656 } 657 658 } else if (ptime->tmspec & TSPEC_DAYOFWEEK) { 659 moredays = 7; 660 } else if (ptime->tmspec & TSPEC_HOUROFDAY) { 661 moredays = 1; 662 } 663 664 if (moredays != 0) { 665 nextmatch.tsecs += SECS_PER_HOUR * 24 * moredays; 666 nextmatch.tm = *(localtime(&nextmatch.tsecs)); 667 } 668 669 /* 670 * The new time will need to be adjusted if the setting of 671 * daylight-saving has changed between the two times. 672 */ 673 ptime_adjust4dst(&nextmatch, ptime); 674 675 /* Everything worked. Update the given time and return. */ 676 *ptime = nextmatch; 677 return (0); 678 } 679 680 int 681 ptimeset_time(struct ptime_data *ptime, time_t secs) 682 { 683 684 if (ptime == NULL) 685 return (-1); 686 687 ptime->tsecs = secs; 688 ptime->tm = *(localtime(&ptime->tsecs)); 689 ptime->parseopts = 0; 690 /* ptime->tmspec = ? */ 691 return (0); 692 } 693