ptimes.c revision 120361
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: head/usr.sbin/newsyslog/ptimes.c 120361 2003-09-23 00:00:26Z gad $"); 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 72struct 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 83static int days_pmonth(int month, int year); 84static int parse8601(struct ptime_data *ptime, const char *str); 85static 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 */ 90static int 91days_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 */ 129static int 130parse8601(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 */ 220static int 221parseDWM(struct ptime_data *ptime, const char *s) 222{ 223 int daysmon, Dseen, WMseen; 224 char *t; 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 switch (*s) { 236 case 'D': 237 if (Dseen) 238 return (-1); 239 Dseen++; 240 ptime->tmspec |= TSPEC_HOUROFDAY; 241 s++; 242 l = strtol(s, &t, 10); 243 if (l < 0 || l > 23) 244 return (-1); 245 tm.tm_hour = l; 246 break; 247 248 case 'W': 249 if (WMseen) 250 return (-1); 251 WMseen++; 252 ptime->tmspec |= TSPEC_DAYOFWEEK; 253 s++; 254 l = strtol(s, &t, 10); 255 if (l < 0 || l > 6) 256 return (-1); 257 if (l != tm.tm_wday) { 258 int save; 259 260 if (l < tm.tm_wday) { 261 save = 6 - tm.tm_wday; 262 save += (l + 1); 263 } else { 264 save = l - tm.tm_wday; 265 } 266 267 tm.tm_mday += save; 268 269 if (tm.tm_mday > daysmon) { 270 tm.tm_mon++; 271 tm.tm_mday = tm.tm_mday - daysmon; 272 } 273 } 274 break; 275 276 case 'M': 277 if (WMseen) 278 return (-1); 279 WMseen++; 280 ptime->tmspec |= TSPEC_DAYOFMONTH; 281 s++; 282 if (tolower(*s) == 'l') { 283 /* User wants the last day of the month. */ 284 ptime->tmspec |= TSPEC_LDAYOFMONTH; 285 tm.tm_mday = daysmon; 286 s++; 287 t = __DECONST(char *,s); 288 } else { 289 l = strtol(s, &t, 10); 290 if (l < 1 || l > 31) 291 return (-1); 292 293 if (l > daysmon) 294 return (-1); 295 tm.tm_mday = l; 296 } 297 break; 298 299 default: 300 return (-1); 301 break; 302 } 303 304 if (*t == '\0' || isspace(*t)) 305 break; 306 else 307 s = t; 308 } 309 310 ptime->tm = tm; 311 return (0); 312} 313 314/* 315 * Initialize a new ptime-related data area. 316 */ 317struct ptime_data * 318ptime_init(const struct ptime_data *optsrc) 319{ 320 struct ptime_data *newdata; 321 322 newdata = malloc(sizeof(struct ptime_data)); 323 if (optsrc != NULL) { 324 memcpy(newdata, optsrc, sizeof(struct ptime_data)); 325 } else { 326 memset(newdata, '\0', sizeof(struct ptime_data)); 327 newdata->did_adj4dst = TNYET_ADJ4DST; 328 } 329 330 return (newdata); 331} 332 333/* 334 * Adjust a given time if that time is in a different timezone than 335 * some other time. 336 */ 337int 338ptime_adjust4dst(struct ptime_data *ptime, const struct ptime_data *dstsrc) 339{ 340 struct ptime_data adjtime; 341 342 if (ptime == NULL) 343 return (-1); 344 345 /* 346 * Changes are not made to the given time until after all 347 * of the calculations have been successful. 348 */ 349 adjtime = *ptime; 350 351 /* Check to see if this adjustment was already made */ 352 if ((adjtime.did_adj4dst != TNYET_ADJ4DST) && 353 (adjtime.did_adj4dst == dstsrc->tm.tm_isdst)) 354 return (0); /* yes, so don't make it twice */ 355 356 /* See if daylight-saving has changed between the two times. */ 357 if (dstsrc->tm.tm_isdst != adjtime.tm.tm_isdst) { 358 if (adjtime.tm.tm_isdst == 1) 359 adjtime.tsecs -= SECS_PER_HOUR; 360 else if (adjtime.tm.tm_isdst == 0) 361 adjtime.tsecs += SECS_PER_HOUR; 362 adjtime.tm = *(localtime(&adjtime.tsecs)); 363 /* Remember that this adjustment has been made */ 364 adjtime.did_adj4dst = dstsrc->tm.tm_isdst; 365 /* 366 * XXX - Should probably check to see if changing the 367 * hour also changed the value of is_dst. What 368 * should we do in that case? 369 */ 370 } 371 372 *ptime = adjtime; 373 return (0); 374} 375 376int 377ptime_relparse(struct ptime_data *ptime, int parseopts, time_t basetime, 378 const char *str) 379{ 380 int dpm, pres; 381 struct tm temp_tm; 382 383 ptime->parseopts = parseopts; 384 ptime->basesecs = basetime; 385 ptime->basetm = *(localtime(&ptime->basesecs)); 386 ptime->tm = ptime->basetm; 387 ptime->tm.tm_hour = ptime->tm.tm_min = ptime->tm.tm_sec = 0; 388 389 /* 390 * Call a routine which sets ptime.tm and ptime.tspecs based 391 * on the given string and parsing-options. Note that the 392 * routine should not call mktime to set ptime.tsecs. 393 */ 394 if (parseopts & PTM_PARSE_DWM) 395 pres = parseDWM(ptime, str); 396 else 397 pres = parse8601(ptime, str); 398 if (pres < 0) { 399 ptime->tsecs = (time_t)pres; 400 return (pres); 401 } 402 403 /* 404 * Before calling mktime, check to see if we ended up with a 405 * "day-of-month" that does not exist in the selected month. 406 * If we did call mktime with that info, then mktime will 407 * make it look like the user specifically requested a day 408 * in the following month (eg: Feb 31 turns into Mar 3rd). 409 */ 410 dpm = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); 411 if ((parseopts & PTM_PARSE_MATCHDOM) && 412 (ptime->tmspec & TSPEC_DAYOFMONTH) && 413 (ptime->tm.tm_mday> dpm)) { 414 /* 415 * ptime_nxtime() will want a ptime->tsecs value, 416 * but we need to avoid mktime resetting all the 417 * ptime->tm values. 418 */ 419 if (verbose && dbg_at_times > 1) 420 fprintf(stderr, 421 "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)", 422 ptime->tm.tm_year, ptime->tm.tm_mon, 423 ptime->tm.tm_mday, ptime->tm.tm_hour, 424 ptime->tm.tm_min, dpm); 425 temp_tm = ptime->tm; 426 ptime->tsecs = mktime(&temp_tm); 427 if (ptime->tsecs > (time_t)-1) 428 ptimeset_nxtime(ptime); 429 if (verbose && dbg_at_times > 1) 430 fprintf(stderr, 431 " to: %4d/%02d/%02d %02d:%02d\n", 432 ptime->tm.tm_year, ptime->tm.tm_mon, 433 ptime->tm.tm_mday, ptime->tm.tm_hour, 434 ptime->tm.tm_min); 435 } 436 437 /* 438 * Convert the ptime.tm into standard time_t seconds. Check 439 * for invalid times, which includes things like the hour lost 440 * when switching from "standard time" to "daylight saving". 441 */ 442 ptime->tsecs = mktime(&ptime->tm); 443 if (ptime->tsecs == (time_t)-1) { 444 ptime->tsecs = (time_t)-2; 445 return (-2); 446 } 447 448 return (0); 449} 450 451int 452ptime_free(struct ptime_data *ptime) 453{ 454 455 if (ptime == NULL) 456 return (-1); 457 458 free(ptime); 459 return (0); 460} 461 462/* 463 * Some trivial routines so ptime_data can remain a completely 464 * opaque type. 465 */ 466const char * 467ptimeget_ctime(const struct ptime_data *ptime) 468{ 469 470 if (ptime == NULL) 471 return ("Null time in ptimeget_ctime()\n"); 472 473 return (ctime(&ptime->tsecs)); 474} 475 476double 477ptimeget_diff(const struct ptime_data *minuend, const struct 478 ptime_data *subtrahend) 479{ 480 481 /* Just like difftime(), we have no good error-return */ 482 if (minuend == NULL || subtrahend == NULL) 483 return (0.0); 484 485 return (difftime(minuend->tsecs, subtrahend->tsecs)); 486} 487 488time_t 489ptimeget_secs(const struct ptime_data *ptime) 490{ 491 492 if (ptime == NULL) 493 return (-1); 494 495 return (ptime->tsecs); 496} 497 498/* 499 * Generate an approximate timestamp for the next event, based on 500 * what parts of time were specified by the original parameter to 501 * ptime_relparse(). The result may be -1 if there is no obvious 502 * "next time" which will work. 503 */ 504int 505ptimeset_nxtime(struct ptime_data *ptime) 506{ 507 int moredays, tdpm, tmon, tyear; 508 struct ptime_data nextmatch; 509 510 if (ptime == NULL) 511 return (-1); 512 513 /* 514 * Changes are not made to the given time until after all 515 * of the calculations have been successful. 516 */ 517 nextmatch = *ptime; 518 /* 519 * If the user specified a year and we're already past that 520 * time, then there will never be another one! 521 */ 522 if (ptime->tmspec & TSPEC_YEAR) 523 return (-1); 524 525 /* 526 * The caller gave us a time in the past. Calculate how much 527 * time is needed to go from that valid rotate time to the 528 * next valid rotate time. We only need to get to the nearest 529 * hour, because newsyslog is only run once per hour. 530 */ 531 moredays = 0; 532 if (ptime->tmspec & TSPEC_MONTHOFYEAR) { 533 /* Special case: Feb 29th does not happen every year. */ 534 if (ptime->tm.tm_mon == 1 && ptime->tm.tm_mday == 29) { 535 nextmatch.tm.tm_year += 4; 536 if (days_pmonth(1, nextmatch.tm.tm_year) < 29) 537 nextmatch.tm.tm_year += 4; 538 } else { 539 nextmatch.tm.tm_year += 1; 540 } 541 nextmatch.tm.tm_isdst = -1; 542 nextmatch.tsecs = mktime(&nextmatch.tm); 543 544 } else if (ptime->tmspec & TSPEC_LDAYOFMONTH) { 545 /* 546 * Need to get to the last day of next month. Origtm is 547 * already at the last day of this month, so just add to 548 * it number of days in the next month. 549 */ 550 if (ptime->tm.tm_mon < 11) 551 moredays = days_pmonth(ptime->tm.tm_mon + 1, 552 ptime->tm.tm_year); 553 else 554 moredays = days_pmonth(0, ptime->tm.tm_year + 1); 555 556 } else if (ptime->tmspec & TSPEC_DAYOFMONTH) { 557 /* Jump to the same day in the next month */ 558 moredays = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); 559 /* 560 * In some cases, the next month may not *have* the 561 * desired day-of-the-month. If that happens, then 562 * move to the next month that does have enough days. 563 */ 564 tmon = ptime->tm.tm_mon; 565 tyear = ptime->tm.tm_year; 566 for (;;) { 567 if (tmon < 11) 568 tmon += 1; 569 else { 570 tmon = 0; 571 tyear += 1; 572 } 573 tdpm = days_pmonth(tmon, tyear); 574 if (tdpm >= ptime->tm.tm_mday) 575 break; 576 moredays += tdpm; 577 } 578 579 } else if (ptime->tmspec & TSPEC_DAYOFWEEK) { 580 moredays = 7; 581 } else if (ptime->tmspec & TSPEC_HOUROFDAY) { 582 moredays = 1; 583 } 584 585 if (moredays != 0) { 586 nextmatch.tsecs += SECS_PER_HOUR * 24 * moredays; 587 nextmatch.tm = *(localtime(&nextmatch.tsecs)); 588 } 589 590 /* 591 * The new time will need to be adjusted if the setting of 592 * daylight-saving has changed between the two times. 593 */ 594 ptime_adjust4dst(&nextmatch, ptime); 595 596 /* Everything worked. Update the given time and return. */ 597 *ptime = nextmatch; 598 return (0); 599} 600 601int 602ptimeset_time(struct ptime_data *ptime, time_t secs) 603{ 604 605 if (ptime == NULL) 606 return (-1); 607 608 ptime->tsecs = secs; 609 ptime->tm = *(localtime(&ptime->tsecs)); 610 ptime->parseopts = 0; 611 /* ptime->tmspec = ? */ 612 return (0); 613} 614