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 } 283 } 284 break; 285 286 case 'M': 287 if (WMseen) 288 return (-1); 289 WMseen++; 290 ptime->tmspec |= TSPEC_DAYOFMONTH; 291 s++; 292 if (tolower(*s) == 'l') { 293 /* User wants the last day of the month. */ 294 ptime->tmspec |= TSPEC_LDAYOFMONTH; 295 tm.tm_mday = daysmon; 296 endval = s + 1; 297 } else { 298 l = strtol(s, &tmp, 10); 299 if (l < 1 || l > 31) 300 return (-1); 301 302 if (l > daysmon) 303 return (-1); 304 endval = tmp; 305 tm.tm_mday = l; 306 } 307 break; 308 309 default: 310 return (-1); 311 break; 312 } 313 314 if (endval == NULL) 315 return (-1); 316 else if (*endval == '\0' || isspace(*endval)) 317 break; 318 else 319 s = endval; 320 } 321 322 ptime->tm = tm; 323 return (0); 324 } 325 326 /* 327 * Initialize a new ptime-related data area. 328 */ 329 struct ptime_data * 330 ptime_init(const struct ptime_data *optsrc) 331 { 332 struct ptime_data *newdata; 333 334 newdata = malloc(sizeof(struct ptime_data)); 335 if (optsrc != NULL) { 336 memcpy(newdata, optsrc, sizeof(struct ptime_data)); 337 } else { 338 memset(newdata, '\0', sizeof(struct ptime_data)); 339 newdata->did_adj4dst = TNYET_ADJ4DST; 340 } 341 342 return (newdata); 343 } 344 345 /* 346 * Adjust a given time if that time is in a different timezone than 347 * some other time. 348 */ 349 int 350 ptime_adjust4dst(struct ptime_data *ptime, const struct ptime_data *dstsrc) 351 { 352 struct ptime_data adjtime; 353 354 if (ptime == NULL) 355 return (-1); 356 357 /* 358 * Changes are not made to the given time until after all 359 * of the calculations have been successful. 360 */ 361 adjtime = *ptime; 362 363 /* Check to see if this adjustment was already made */ 364 if ((adjtime.did_adj4dst != TNYET_ADJ4DST) && 365 (adjtime.did_adj4dst == dstsrc->tm.tm_isdst)) 366 return (0); /* yes, so don't make it twice */ 367 368 /* See if daylight-saving has changed between the two times. */ 369 if (dstsrc->tm.tm_isdst != adjtime.tm.tm_isdst) { 370 if (adjtime.tm.tm_isdst == 1) 371 adjtime.tsecs -= SECS_PER_HOUR; 372 else if (adjtime.tm.tm_isdst == 0) 373 adjtime.tsecs += SECS_PER_HOUR; 374 adjtime.tm = *(localtime(&adjtime.tsecs)); 375 /* Remember that this adjustment has been made */ 376 adjtime.did_adj4dst = dstsrc->tm.tm_isdst; 377 /* 378 * XXX - Should probably check to see if changing the 379 * hour also changed the value of is_dst. What 380 * should we do in that case? 381 */ 382 } 383 384 *ptime = adjtime; 385 return (0); 386 } 387 388 int 389 ptime_relparse(struct ptime_data *ptime, int parseopts, time_t basetime, 390 const char *str) 391 { 392 int dpm, pres; 393 struct tm temp_tm; 394 395 ptime->parseopts = parseopts; 396 ptime->basesecs = basetime; 397 ptime->basetm = *(localtime(&ptime->basesecs)); 398 ptime->tm = ptime->basetm; 399 ptime->tm.tm_hour = ptime->tm.tm_min = ptime->tm.tm_sec = 0; 400 401 /* 402 * Call a routine which sets ptime.tm and ptime.tspecs based 403 * on the given string and parsing-options. Note that the 404 * routine should not call mktime to set ptime.tsecs. 405 */ 406 if (parseopts & PTM_PARSE_DWM) 407 pres = parseDWM(ptime, str); 408 else 409 pres = parse8601(ptime, str); 410 if (pres < 0) { 411 ptime->tsecs = (time_t)pres; 412 return (pres); 413 } 414 415 /* 416 * Before calling mktime, check to see if we ended up with a 417 * "day-of-month" that does not exist in the selected month. 418 * If we did call mktime with that info, then mktime will 419 * make it look like the user specifically requested a day 420 * in the following month (eg: Feb 31 turns into Mar 3rd). 421 */ 422 dpm = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); 423 if ((parseopts & PTM_PARSE_MATCHDOM) && 424 (ptime->tmspec & TSPEC_DAYOFMONTH) && 425 (ptime->tm.tm_mday> dpm)) { 426 /* 427 * ptime_nxtime() will want a ptime->tsecs value, 428 * but we need to avoid mktime resetting all the 429 * ptime->tm values. 430 */ 431 if (verbose && dbg_at_times > 1) 432 fprintf(stderr, 433 "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)", 434 ptime->tm.tm_year, ptime->tm.tm_mon, 435 ptime->tm.tm_mday, ptime->tm.tm_hour, 436 ptime->tm.tm_min, dpm); 437 temp_tm = ptime->tm; 438 ptime->tsecs = mktime(&temp_tm); 439 if (ptime->tsecs > (time_t)-1) 440 ptimeset_nxtime(ptime); 441 if (verbose && dbg_at_times > 1) 442 fprintf(stderr, 443 " to: %4d/%02d/%02d %02d:%02d\n", 444 ptime->tm.tm_year, ptime->tm.tm_mon, 445 ptime->tm.tm_mday, ptime->tm.tm_hour, 446 ptime->tm.tm_min); 447 } 448 449 /* 450 * Convert the ptime.tm into standard time_t seconds. Check 451 * for invalid times, which includes things like the hour lost 452 * when switching from "standard time" to "daylight saving". 453 */ 454 ptime->tsecs = mktime(&ptime->tm); 455 if (ptime->tsecs == (time_t)-1) { 456 ptime->tsecs = (time_t)-2; 457 return (-2); 458 } 459 460 return (0); 461 } 462 463 int 464 ptime_free(struct ptime_data *ptime) 465 { 466 467 if (ptime == NULL) 468 return (-1); 469 470 free(ptime); 471 return (0); 472 } 473 474 /* 475 * Some trivial routines so ptime_data can remain a completely 476 * opaque type. 477 */ 478 const char * 479 ptimeget_ctime(const struct ptime_data *ptime) 480 { 481 482 if (ptime == NULL) 483 return ("Null time in ptimeget_ctime()\n"); 484 485 return (ctime(&ptime->tsecs)); 486 } 487 488 /* 489 * Generate a time of day string in an RFC5424 compatible format. Return a 490 * pointer to the buffer with the timestamp string or NULL if an error. If the 491 * time is not supplied, cannot be converted to local time, or the resulting 492 * string would overflow the buffer, the returned string will be the RFC5424 493 * NILVALUE. 494 */ 495 char * 496 ptimeget_ctime_rfc5424(const struct ptime_data *ptime, 497 char *timebuf, size_t bufsize) 498 { 499 static const char NILVALUE[] = {"-"}; /* RFC5424 specified NILVALUE */ 500 int chars; 501 struct tm tm; 502 int tz_hours; 503 int tz_mins; 504 long tz_offset; 505 char tz_sign; 506 507 if (timebuf == NULL) { 508 return (NULL); 509 } 510 511 if (bufsize < sizeof(NILVALUE)) { 512 return (NULL); 513 } 514 515 /* 516 * Convert to localtime. RFC5424 mandates the use of the NILVALUE if 517 * the time cannot be obtained, so use that if there is an error in the 518 * conversion. 519 */ 520 if (ptime == NULL || localtime_r(&(ptime->tsecs), &tm) == NULL) { 521 strlcpy(timebuf, NILVALUE, bufsize); 522 return (timebuf); 523 } 524 525 /* 526 * Convert the time to a string in RFC5424 format. The conversion 527 * cannot be done with strftime() because it cannot produce the correct 528 * timezone offset format. 529 */ 530 if (tm.tm_gmtoff < 0) { 531 tz_sign = '-'; 532 tz_offset = -tm.tm_gmtoff; 533 } else { 534 tz_sign = '+'; 535 tz_offset = tm.tm_gmtoff; 536 } 537 538 tz_hours = tz_offset / 3600; 539 tz_mins = (tz_offset % 3600) / 60; 540 541 chars = snprintf(timebuf, bufsize, 542 "%04d-%02d-%02d" /* date */ 543 "T%02d:%02d:%02d" /* time */ 544 "%c%02d:%02d", /* time zone offset */ 545 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, 546 tm.tm_hour, tm.tm_min, tm.tm_sec, 547 tz_sign, tz_hours, tz_mins); 548 549 /* If the timestamp is too big for timebuf, return the NILVALUE. */ 550 if (chars >= (int)bufsize) { 551 strlcpy(timebuf, NILVALUE, bufsize); 552 } 553 554 return (timebuf); 555 } 556 557 double 558 ptimeget_diff(const struct ptime_data *minuend, const struct 559 ptime_data *subtrahend) 560 { 561 562 /* Just like difftime(), we have no good error-return */ 563 if (minuend == NULL || subtrahend == NULL) 564 return (0.0); 565 566 return (difftime(minuend->tsecs, subtrahend->tsecs)); 567 } 568 569 time_t 570 ptimeget_secs(const struct ptime_data *ptime) 571 { 572 573 if (ptime == NULL) 574 return (-1); 575 576 return (ptime->tsecs); 577 } 578 579 /* 580 * Generate an approximate timestamp for the next event, based on 581 * what parts of time were specified by the original parameter to 582 * ptime_relparse(). The result may be -1 if there is no obvious 583 * "next time" which will work. 584 */ 585 int 586 ptimeset_nxtime(struct ptime_data *ptime) 587 { 588 int moredays, tdpm, tmon, tyear; 589 struct ptime_data nextmatch; 590 591 if (ptime == NULL) 592 return (-1); 593 594 /* 595 * Changes are not made to the given time until after all 596 * of the calculations have been successful. 597 */ 598 nextmatch = *ptime; 599 /* 600 * If the user specified a year and we're already past that 601 * time, then there will never be another one! 602 */ 603 if (ptime->tmspec & TSPEC_YEAR) 604 return (-1); 605 606 /* 607 * The caller gave us a time in the past. Calculate how much 608 * time is needed to go from that valid rotate time to the 609 * next valid rotate time. We only need to get to the nearest 610 * hour, because newsyslog is only run once per hour. 611 */ 612 moredays = 0; 613 if (ptime->tmspec & TSPEC_MONTHOFYEAR) { 614 /* Special case: Feb 29th does not happen every year. */ 615 if (ptime->tm.tm_mon == 1 && ptime->tm.tm_mday == 29) { 616 nextmatch.tm.tm_year += 4; 617 if (days_pmonth(1, nextmatch.tm.tm_year) < 29) 618 nextmatch.tm.tm_year += 4; 619 } else { 620 nextmatch.tm.tm_year += 1; 621 } 622 nextmatch.tm.tm_isdst = -1; 623 nextmatch.tsecs = mktime(&nextmatch.tm); 624 625 } else if (ptime->tmspec & TSPEC_LDAYOFMONTH) { 626 /* 627 * Need to get to the last day of next month. Origtm is 628 * already at the last day of this month, so just add to 629 * it number of days in the next month. 630 */ 631 if (ptime->tm.tm_mon < 11) 632 moredays = days_pmonth(ptime->tm.tm_mon + 1, 633 ptime->tm.tm_year); 634 else 635 moredays = days_pmonth(0, ptime->tm.tm_year + 1); 636 637 } else if (ptime->tmspec & TSPEC_DAYOFMONTH) { 638 /* Jump to the same day in the next month */ 639 moredays = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); 640 /* 641 * In some cases, the next month may not *have* the 642 * desired day-of-the-month. If that happens, then 643 * move to the next month that does have enough days. 644 */ 645 tmon = ptime->tm.tm_mon; 646 tyear = ptime->tm.tm_year; 647 for (;;) { 648 if (tmon < 11) 649 tmon += 1; 650 else { 651 tmon = 0; 652 tyear += 1; 653 } 654 tdpm = days_pmonth(tmon, tyear); 655 if (tdpm >= ptime->tm.tm_mday) 656 break; 657 moredays += tdpm; 658 } 659 660 } else if (ptime->tmspec & TSPEC_DAYOFWEEK) { 661 moredays = 7; 662 } else if (ptime->tmspec & TSPEC_HOUROFDAY) { 663 moredays = 1; 664 } 665 666 if (moredays != 0) { 667 nextmatch.tsecs += SECS_PER_HOUR * 24 * moredays; 668 nextmatch.tm = *(localtime(&nextmatch.tsecs)); 669 } 670 671 /* 672 * The new time will need to be adjusted if the setting of 673 * daylight-saving has changed between the two times. 674 */ 675 ptime_adjust4dst(&nextmatch, ptime); 676 677 /* Everything worked. Update the given time and return. */ 678 *ptime = nextmatch; 679 return (0); 680 } 681 682 int 683 ptimeset_time(struct ptime_data *ptime, time_t secs) 684 { 685 686 if (ptime == NULL) 687 return (-1); 688 689 ptime->tsecs = secs; 690 ptime->tm = *(localtime(&ptime->tsecs)); 691 ptime->parseopts = 0; 692 /* ptime->tmspec = ? */ 693 return (0); 694 } 695