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