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>.
8 * Copyright (c) 2003 - Garance Alistair Drosehn <gad@FreeBSD.org>.
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions
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.
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
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.
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 * ------+---------+---------+---------+---------+---------+---------+---------*
42 * $FreeBSD: src/usr.sbin/newsyslog/ptimes.c,v 1.5 2004/06/07 21:53:27 gad Exp $
43 * $DragonFly: src/usr.sbin/newsyslog/ptimes.c,v 1.1 2007/05/12 08:52:00 swildner Exp $
55 #define SECS_PER_HOUR 3600
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.
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
69 #define TNYET_ADJ4DST -10 /* DST has "not yet" been adjusted */
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 */
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
);
87 * Simple routine to calculate the number of days in a given month.
90 days_pmonth(int month
, int year
)
92 static const int mtab
[] = {31, 28, 31, 30, 31, 30, 31, 31,
100 * We are usually called with a 'tm-year' value
101 * (ie, the value = the number of years past 1900).
107 * This is a leap year, as long as it is not a
108 * multiple of 100, or if it is a multiple of
112 ndays
++; /* not multiple of 100 */
113 else if (year
% 400 == 0)
114 ndays
++; /* is multiple of 100 and 400 */
121 * Parse a limited subset of ISO 8601. The specific format is as follows:
123 * [CC[YY[MM[DD]]]][THH[MM[SS]]] (where `T' is the literal letter)
125 * We don't accept a timezone specification; missing fields (including timezone)
126 * are defaulted to the current date but time zero.
129 parse8601(struct ptime_data
*ptime
, const char *s
)
135 l
= strtol(s
, &t
, 10);
136 if (l
< 0 || l
>= INT_MAX
|| (*t
!= '\0' && *t
!= 'T'))
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.
145 ptime
->tmspec
= TSPEC_HOUROFDAY
;
148 tm
.tm_year
= ((l
/ 1000000) - 19) * 100;
151 ptime
->tmspec
|= TSPEC_YEAR
;
152 tm
.tm_year
-= tm
.tm_year
% 100;
153 tm
.tm_year
+= l
/ 10000;
156 ptime
->tmspec
|= TSPEC_MONTHOFYEAR
;
157 tm
.tm_mon
= (l
/ 100) - 1;
160 ptime
->tmspec
|= TSPEC_DAYOFMONTH
;
169 if (tm
.tm_year
< 70 || tm
.tm_mon
< 0 || tm
.tm_mon
> 12
170 || tm
.tm_mday
< 1 || tm
.tm_mday
> 31)
175 l
= strtol(s
, &t
, 10);
176 if (l
< 0 || l
>= INT_MAX
|| (*t
!= '\0' && !isspace(*t
)))
187 ptime
->tmspec
|= TSPEC_HOUROFDAY
;
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)
206 * Parse a cyclic time specification, the format is as follows:
208 * [Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
210 * to rotate a logfile cyclic at
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)
216 * We don't accept a timezone specification; missing fields
217 * are defaulted to the current date but time zero.
220 parseDWM(struct ptime_data
*ptime
, const char *s
)
222 int daysmon
, Dseen
, WMseen
;
228 /* Save away the number of days in this month */
230 daysmon
= days_pmonth(tm
.tm_mon
, tm
.tm_year
);
233 ptime
->tmspec
= TSPEC_HOUROFDAY
;
241 ptime
->tmspec
|= TSPEC_HOUROFDAY
;
243 l
= strtol(s
, &tmp
, 10);
254 ptime
->tmspec
|= TSPEC_DAYOFWEEK
;
256 l
= strtol(s
, &tmp
, 10);
260 if (l
!= tm
.tm_wday
) {
263 if (l
< tm
.tm_wday
) {
264 save
= 6 - tm
.tm_wday
;
267 save
= l
- tm
.tm_wday
;
272 if (tm
.tm_mday
> daysmon
) {
274 tm
.tm_mday
= tm
.tm_mday
- daysmon
;
283 ptime
->tmspec
|= TSPEC_DAYOFMONTH
;
285 if (tolower(*s
) == 'l') {
286 /* User wants the last day of the month. */
287 ptime
->tmspec
|= TSPEC_LDAYOFMONTH
;
288 tm
.tm_mday
= daysmon
;
291 l
= strtol(s
, &tmp
, 10);
309 else if (*endval
== '\0' || isspace(*endval
))
320 * Initialize a new ptime-related data area.
323 ptime_init(const struct ptime_data
*optsrc
)
325 struct ptime_data
*newdata
;
327 newdata
= malloc(sizeof(struct ptime_data
));
328 if (optsrc
!= NULL
) {
329 memcpy(newdata
, optsrc
, sizeof(struct ptime_data
));
331 memset(newdata
, '\0', sizeof(struct ptime_data
));
332 newdata
->did_adj4dst
= TNYET_ADJ4DST
;
339 * Adjust a given time if that time is in a different timezone than
343 ptime_adjust4dst(struct ptime_data
*ptime
, const struct ptime_data
*dstsrc
)
345 struct ptime_data adjtim
;
351 * Changes are not made to the given time until after all
352 * of the calculations have been successful.
356 /* Check to see if this adjustment was already made */
357 if ((adjtim
.did_adj4dst
!= TNYET_ADJ4DST
) &&
358 (adjtim
.did_adj4dst
== dstsrc
->tm
.tm_isdst
))
359 return (0); /* yes, so don't make it twice */
361 /* See if daylight-saving has changed between the two times. */
362 if (dstsrc
->tm
.tm_isdst
!= adjtim
.tm
.tm_isdst
) {
363 if (adjtim
.tm
.tm_isdst
== 1)
364 adjtim
.tsecs
-= SECS_PER_HOUR
;
365 else if (adjtim
.tm
.tm_isdst
== 0)
366 adjtim
.tsecs
+= SECS_PER_HOUR
;
367 adjtim
.tm
= *(localtime(&adjtim
.tsecs
));
368 /* Remember that this adjustment has been made */
369 adjtim
.did_adj4dst
= dstsrc
->tm
.tm_isdst
;
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?
382 ptime_relparse(struct ptime_data
*ptime
, int parseopts
, time_t basetime
,
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;
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.
399 if (parseopts
& PTM_PARSE_DWM
)
400 pres
= parseDWM(ptime
, str
);
402 pres
= parse8601(ptime
, str
);
404 ptime
->tsecs
= (time_t)pres
;
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).
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
)) {
420 * ptime_nxtime() will want a ptime->tsecs value,
421 * but we need to avoid mktime resetting all the
424 if (verbose
&& dbg_at_times
> 1)
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
);
431 ptime
->tsecs
= mktime(&temp_tm
);
432 if (ptime
->tsecs
> (time_t)-1)
433 ptimeset_nxtime(ptime
);
434 if (verbose
&& dbg_at_times
> 1)
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
,
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".
447 ptime
->tsecs
= mktime(&ptime
->tm
);
448 if (ptime
->tsecs
== (time_t)-1) {
449 ptime
->tsecs
= (time_t)-2;
457 ptime_free(struct ptime_data
*ptime
)
468 * Some trivial routines so ptime_data can remain a completely
472 ptimeget_ctime(const struct ptime_data
*ptime
)
476 return ("Null time in ptimeget_ctime()\n");
478 return (ctime(&ptime
->tsecs
));
482 ptimeget_diff(const struct ptime_data
*minuend
, const struct
483 ptime_data
*subtrahend
)
486 /* Just like difftime(), we have no good error-return */
487 if (minuend
== NULL
|| subtrahend
== NULL
)
490 return (difftime(minuend
->tsecs
, subtrahend
->tsecs
));
494 ptimeget_secs(const struct ptime_data
*ptime
)
500 return (ptime
->tsecs
);
504 * Generate an approximate timestamp for the next event, based on
505 * what parts of time were specified by the original parameter to
506 * ptime_relparse(). The result may be -1 if there is no obvious
507 * "next time" which will work.
510 ptimeset_nxtime(struct ptime_data
*ptime
)
512 int moredays
, tdpm
, tmon
, tyear
;
513 struct ptime_data nextmatch
;
519 * Changes are not made to the given time until after all
520 * of the calculations have been successful.
524 * If the user specified a year and we're already past that
525 * time, then there will never be another one!
527 if (ptime
->tmspec
& TSPEC_YEAR
)
531 * The caller gave us a time in the past. Calculate how much
532 * time is needed to go from that valid rotate time to the
533 * next valid rotate time. We only need to get to the nearest
534 * hour, because newsyslog is only run once per hour.
537 if (ptime
->tmspec
& TSPEC_MONTHOFYEAR
) {
538 /* Special case: Feb 29th does not happen every year. */
539 if (ptime
->tm
.tm_mon
== 1 && ptime
->tm
.tm_mday
== 29) {
540 nextmatch
.tm
.tm_year
+= 4;
541 if (days_pmonth(1, nextmatch
.tm
.tm_year
) < 29)
542 nextmatch
.tm
.tm_year
+= 4;
544 nextmatch
.tm
.tm_year
+= 1;
546 nextmatch
.tm
.tm_isdst
= -1;
547 nextmatch
.tsecs
= mktime(&nextmatch
.tm
);
549 } else if (ptime
->tmspec
& TSPEC_LDAYOFMONTH
) {
551 * Need to get to the last day of next month. Origtm is
552 * already at the last day of this month, so just add to
553 * it number of days in the next month.
555 if (ptime
->tm
.tm_mon
< 11)
556 moredays
= days_pmonth(ptime
->tm
.tm_mon
+ 1,
559 moredays
= days_pmonth(0, ptime
->tm
.tm_year
+ 1);
561 } else if (ptime
->tmspec
& TSPEC_DAYOFMONTH
) {
562 /* Jump to the same day in the next month */
563 moredays
= days_pmonth(ptime
->tm
.tm_mon
, ptime
->tm
.tm_year
);
565 * In some cases, the next month may not *have* the
566 * desired day-of-the-month. If that happens, then
567 * move to the next month that does have enough days.
569 tmon
= ptime
->tm
.tm_mon
;
570 tyear
= ptime
->tm
.tm_year
;
578 tdpm
= days_pmonth(tmon
, tyear
);
579 if (tdpm
>= ptime
->tm
.tm_mday
)
584 } else if (ptime
->tmspec
& TSPEC_DAYOFWEEK
) {
586 } else if (ptime
->tmspec
& TSPEC_HOUROFDAY
) {
591 nextmatch
.tsecs
+= SECS_PER_HOUR
* 24 * moredays
;
592 nextmatch
.tm
= *(localtime(&nextmatch
.tsecs
));
596 * The new time will need to be adjusted if the setting of
597 * daylight-saving has changed between the two times.
599 ptime_adjust4dst(&nextmatch
, ptime
);
601 /* Everything worked. Update the given time and return. */
607 ptimeset_time(struct ptime_data
*ptime
, time_t secs
)
614 ptime
->tm
= *(localtime(&ptime
->tsecs
));
615 ptime
->parseopts
= 0;
616 /* ptime->tmspec = ? */