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