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