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