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: head/usr.sbin/newsyslog/ptimes.c 318960 2017-05-26 16:36:30Z dab $
54 #define SECS_PER_HOUR 3600
57 * Bit-values which indicate which components of time were specified
58 * by the string given to parse8601 or parseDWM. These are needed to
59 * calculate what time-in-the-future will match that string.
61 #define TSPEC_YEAR 0x0001
62 #define TSPEC_MONTHOFYEAR 0x0002
63 #define TSPEC_LDAYOFMONTH 0x0004
64 #define TSPEC_DAYOFMONTH 0x0008
65 #define TSPEC_DAYOFWEEK 0x0010
66 #define TSPEC_HOUROFDAY 0x0020
68 #define TNYET_ADJ4DST -10 /* DST has "not yet" been adjusted */
71 time_t basesecs
; /* Base point for relative times */
72 time_t tsecs
; /* Time in seconds */
73 struct tm basetm
; /* Base Time expanded into fields */
74 struct tm tm
; /* Time expanded into fields */
75 int did_adj4dst
; /* Track calls to ptime_adjust4dst */
76 int parseopts
; /* Options given for parsing */
77 int tmspec
; /* Indicates which time fields had
78 * been specified by the user */
81 static int days_pmonth(int month
, int year
);
82 static int parse8601(struct ptime_data
*ptime
, const char *str
);
83 static int parseDWM(struct ptime_data
*ptime
, const char *str
);
86 * Simple routine to calculate the number of days in a given month.
89 days_pmonth(int month
, int year
)
91 static const int mtab
[] = {31, 28, 31, 30, 31, 30, 31, 31,
99 * We are usually called with a 'tm-year' value
100 * (ie, the value = the number of years past 1900).
106 * This is a leap year, as long as it is not a
107 * multiple of 100, or if it is a multiple of
111 ndays
++; /* not multiple of 100 */
112 else if (year
% 400 == 0)
113 ndays
++; /* is multiple of 100 and 400 */
120 * Parse a limited subset of ISO 8601. The specific format is as follows:
122 * [CC[YY[MM[DD]]]][THH[MM[SS]]] (where `T' is the literal letter)
124 * We don't accept a timezone specification; missing fields (including timezone)
125 * are defaulted to the current date but time zero.
128 parse8601(struct ptime_data
*ptime
, const char *s
)
134 l
= strtol(s
, &t
, 10);
135 if (l
< 0 || l
>= INT_MAX
|| (*t
!= '\0' && *t
!= 'T'))
139 * Now t points either to the end of the string (if no time was
140 * provided) or to the letter `T' which separates date and time in
141 * ISO 8601. The pointer arithmetic is the same for either case.
144 ptime
->tmspec
= TSPEC_HOUROFDAY
;
147 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;
157 ptime
->tmspec
|= TSPEC_MONTHOFYEAR
;
158 tm
.tm_mon
= (l
/ 100) - 1;
162 ptime
->tmspec
|= TSPEC_DAYOFMONTH
;
172 if (tm
.tm_year
< 70 || tm
.tm_mon
< 0 || tm
.tm_mon
> 12
173 || tm
.tm_mday
< 1 || tm
.tm_mday
> 31)
178 l
= strtol(s
, &t
, 10);
179 if (l
< 0 || l
>= INT_MAX
|| (*t
!= '\0' && !isspace(*t
)))
192 ptime
->tmspec
|= TSPEC_HOUROFDAY
;
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)
211 * Parse a cyclic time specification, the format is as follows:
213 * [Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
215 * to rotate a logfile cyclic at
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)
221 * We don't accept a timezone specification; missing fields
222 * are defaulted to the current date but time zero.
225 parseDWM(struct ptime_data
*ptime
, const char *s
)
227 int daysmon
, Dseen
, WMseen
;
233 /* Save away the number of days in this month */
235 daysmon
= days_pmonth(tm
.tm_mon
, tm
.tm_year
);
238 ptime
->tmspec
= TSPEC_HOUROFDAY
;
246 ptime
->tmspec
|= TSPEC_HOUROFDAY
;
248 l
= strtol(s
, &tmp
, 10);
259 ptime
->tmspec
|= TSPEC_DAYOFWEEK
;
261 l
= strtol(s
, &tmp
, 10);
265 if (l
!= tm
.tm_wday
) {
268 if (l
< tm
.tm_wday
) {
269 save
= 6 - tm
.tm_wday
;
272 save
= l
- tm
.tm_wday
;
277 if (tm
.tm_mday
> daysmon
) {
279 tm
.tm_mday
= tm
.tm_mday
- daysmon
;
288 ptime
->tmspec
|= TSPEC_DAYOFMONTH
;
290 if (tolower(*s
) == 'l') {
291 /* User wants the last day of the month. */
292 ptime
->tmspec
|= TSPEC_LDAYOFMONTH
;
293 tm
.tm_mday
= daysmon
;
296 l
= strtol(s
, &tmp
, 10);
314 else if (*endval
== '\0' || isspace(*endval
))
325 * Initialize a new ptime-related data area.
328 ptime_init(const struct ptime_data
*optsrc
)
330 struct ptime_data
*newdata
;
332 newdata
= malloc(sizeof(struct ptime_data
));
333 if (optsrc
!= NULL
) {
334 memcpy(newdata
, optsrc
, sizeof(struct ptime_data
));
336 memset(newdata
, '\0', sizeof(struct ptime_data
));
337 newdata
->did_adj4dst
= TNYET_ADJ4DST
;
344 * Adjust a given time if that time is in a different timezone than
348 ptime_adjust4dst(struct ptime_data
*ptime
, const struct ptime_data
*dstsrc
)
350 struct ptime_data adjtime
;
356 * Changes are not made to the given time until after all
357 * of the calculations have been successful.
361 /* Check to see if this adjustment was already made */
362 if ((adjtime
.did_adj4dst
!= TNYET_ADJ4DST
) &&
363 (adjtime
.did_adj4dst
== dstsrc
->tm
.tm_isdst
))
364 return (0); /* yes, so don't make it twice */
366 /* See if daylight-saving has changed between the two times. */
367 if (dstsrc
->tm
.tm_isdst
!= adjtime
.tm
.tm_isdst
) {
368 if (adjtime
.tm
.tm_isdst
== 1)
369 adjtime
.tsecs
-= SECS_PER_HOUR
;
370 else if (adjtime
.tm
.tm_isdst
== 0)
371 adjtime
.tsecs
+= SECS_PER_HOUR
;
372 adjtime
.tm
= *(localtime(&adjtime
.tsecs
));
373 /* Remember that this adjustment has been made */
374 adjtime
.did_adj4dst
= dstsrc
->tm
.tm_isdst
;
376 * XXX - Should probably check to see if changing the
377 * hour also changed the value of is_dst. What
378 * should we do in that case?
387 ptime_relparse(struct ptime_data
*ptime
, int parseopts
, time_t basetime
,
393 ptime
->parseopts
= parseopts
;
394 ptime
->basesecs
= basetime
;
395 ptime
->basetm
= *(localtime(&ptime
->basesecs
));
396 ptime
->tm
= ptime
->basetm
;
397 ptime
->tm
.tm_hour
= ptime
->tm
.tm_min
= ptime
->tm
.tm_sec
= 0;
400 * Call a routine which sets ptime.tm and ptime.tspecs based
401 * on the given string and parsing-options. Note that the
402 * routine should not call mktime to set ptime.tsecs.
404 if (parseopts
& PTM_PARSE_DWM
)
405 pres
= parseDWM(ptime
, str
);
407 pres
= parse8601(ptime
, str
);
409 ptime
->tsecs
= (time_t)pres
;
414 * Before calling mktime, check to see if we ended up with a
415 * "day-of-month" that does not exist in the selected month.
416 * If we did call mktime with that info, then mktime will
417 * make it look like the user specifically requested a day
418 * in the following month (eg: Feb 31 turns into Mar 3rd).
420 dpm
= days_pmonth(ptime
->tm
.tm_mon
, ptime
->tm
.tm_year
);
421 if ((parseopts
& PTM_PARSE_MATCHDOM
) &&
422 (ptime
->tmspec
& TSPEC_DAYOFMONTH
) &&
423 (ptime
->tm
.tm_mday
> dpm
)) {
425 * ptime_nxtime() will want a ptime->tsecs value,
426 * but we need to avoid mktime resetting all the
429 if (verbose
&& dbg_at_times
> 1)
431 "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)",
432 ptime
->tm
.tm_year
, ptime
->tm
.tm_mon
,
433 ptime
->tm
.tm_mday
, ptime
->tm
.tm_hour
,
434 ptime
->tm
.tm_min
, dpm
);
436 ptime
->tsecs
= mktime(&temp_tm
);
437 if (ptime
->tsecs
> (time_t)-1)
438 ptimeset_nxtime(ptime
);
439 if (verbose
&& dbg_at_times
> 1)
441 " to: %4d/%02d/%02d %02d:%02d\n",
442 ptime
->tm
.tm_year
, ptime
->tm
.tm_mon
,
443 ptime
->tm
.tm_mday
, ptime
->tm
.tm_hour
,
448 * Convert the ptime.tm into standard time_t seconds. Check
449 * for invalid times, which includes things like the hour lost
450 * when switching from "standard time" to "daylight saving".
452 ptime
->tsecs
= mktime(&ptime
->tm
);
453 if (ptime
->tsecs
== (time_t)-1) {
454 ptime
->tsecs
= (time_t)-2;
462 ptime_free(struct ptime_data
*ptime
)
473 * Some trivial routines so ptime_data can remain a completely
477 ptimeget_ctime(const struct ptime_data
*ptime
)
481 return ("Null time in ptimeget_ctime()\n");
483 return (ctime(&ptime
->tsecs
));
487 * Generate a time of day string in an RFC5424 compatible format. Return a
488 * pointer to the buffer with the timestamp string or NULL if an error. If the
489 * time is not supplied, cannot be converted to local time, or the resulting
490 * string would overflow the buffer, the returned string will be the RFC5424
494 ptimeget_ctime_rfc5424(const struct ptime_data
*ptime
,
495 char *timebuf
, size_t bufsize
)
497 static const char NILVALUE
[] = {"-"}; /* RFC5424 specified NILVALUE */
505 if (timebuf
== NULL
) {
509 if (bufsize
< sizeof(NILVALUE
)) {
514 * Convert to localtime. RFC5424 mandates the use of the NILVALUE if
515 * the time cannot be obtained, so use that if there is an error in the
518 if (ptime
== NULL
|| localtime_r(&(ptime
->tsecs
), &tm
) == NULL
) {
519 strlcpy(timebuf
, NILVALUE
, bufsize
);
524 * Convert the time to a string in RFC5424 format. The conversion
525 * cannot be done with strftime() because it cannot produce the correct
526 * timezone offset format.
528 if (tm
.tm_gmtoff
< 0) {
530 tz_offset
= -tm
.tm_gmtoff
;
533 tz_offset
= tm
.tm_gmtoff
;
536 tz_hours
= tz_offset
/ 3600;
537 tz_mins
= (tz_offset
% 3600) / 60;
539 chars
= snprintf(timebuf
, bufsize
,
540 "%04d-%02d-%02d" /* date */
541 "T%02d:%02d:%02d" /* time */
542 "%c%02d:%02d", /* time zone offset */
543 tm
.tm_year
+ 1900, tm
.tm_mon
+ 1, tm
.tm_mday
,
544 tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
,
545 tz_sign
, tz_hours
, tz_mins
);
547 /* If the timestamp is too big for timebuf, return the NILVALUE. */
548 if (chars
>= (int)bufsize
) {
549 strlcpy(timebuf
, NILVALUE
, bufsize
);
556 ptimeget_diff(const struct ptime_data
*minuend
, const struct
557 ptime_data
*subtrahend
)
560 /* Just like difftime(), we have no good error-return */
561 if (minuend
== NULL
|| subtrahend
== NULL
)
564 return (difftime(minuend
->tsecs
, subtrahend
->tsecs
));
568 ptimeget_secs(const struct ptime_data
*ptime
)
574 return (ptime
->tsecs
);
578 * Generate an approximate timestamp for the next event, based on
579 * what parts of time were specified by the original parameter to
580 * ptime_relparse(). The result may be -1 if there is no obvious
581 * "next time" which will work.
584 ptimeset_nxtime(struct ptime_data
*ptime
)
586 int moredays
, tdpm
, tmon
, tyear
;
587 struct ptime_data nextmatch
;
593 * Changes are not made to the given time until after all
594 * of the calculations have been successful.
598 * If the user specified a year and we're already past that
599 * time, then there will never be another one!
601 if (ptime
->tmspec
& TSPEC_YEAR
)
605 * The caller gave us a time in the past. Calculate how much
606 * time is needed to go from that valid rotate time to the
607 * next valid rotate time. We only need to get to the nearest
608 * hour, because newsyslog is only run once per hour.
611 if (ptime
->tmspec
& TSPEC_MONTHOFYEAR
) {
612 /* Special case: Feb 29th does not happen every year. */
613 if (ptime
->tm
.tm_mon
== 1 && ptime
->tm
.tm_mday
== 29) {
614 nextmatch
.tm
.tm_year
+= 4;
615 if (days_pmonth(1, nextmatch
.tm
.tm_year
) < 29)
616 nextmatch
.tm
.tm_year
+= 4;
618 nextmatch
.tm
.tm_year
+= 1;
620 nextmatch
.tm
.tm_isdst
= -1;
621 nextmatch
.tsecs
= mktime(&nextmatch
.tm
);
623 } else if (ptime
->tmspec
& TSPEC_LDAYOFMONTH
) {
625 * Need to get to the last day of next month. Origtm is
626 * already at the last day of this month, so just add to
627 * it number of days in the next month.
629 if (ptime
->tm
.tm_mon
< 11)
630 moredays
= days_pmonth(ptime
->tm
.tm_mon
+ 1,
633 moredays
= days_pmonth(0, ptime
->tm
.tm_year
+ 1);
635 } else if (ptime
->tmspec
& TSPEC_DAYOFMONTH
) {
636 /* Jump to the same day in the next month */
637 moredays
= days_pmonth(ptime
->tm
.tm_mon
, ptime
->tm
.tm_year
);
639 * In some cases, the next month may not *have* the
640 * desired day-of-the-month. If that happens, then
641 * move to the next month that does have enough days.
643 tmon
= ptime
->tm
.tm_mon
;
644 tyear
= ptime
->tm
.tm_year
;
652 tdpm
= days_pmonth(tmon
, tyear
);
653 if (tdpm
>= ptime
->tm
.tm_mday
)
658 } else if (ptime
->tmspec
& TSPEC_DAYOFWEEK
) {
660 } else if (ptime
->tmspec
& TSPEC_HOUROFDAY
) {
665 nextmatch
.tsecs
+= SECS_PER_HOUR
* 24 * moredays
;
666 nextmatch
.tm
= *(localtime(&nextmatch
.tsecs
));
670 * The new time will need to be adjusted if the setting of
671 * daylight-saving has changed between the two times.
673 ptime_adjust4dst(&nextmatch
, ptime
);
675 /* Everything worked. Update the given time and return. */
681 ptimeset_time(struct ptime_data
*ptime
, time_t secs
)
688 ptime
->tm
= *(localtime(&ptime
->tsecs
));
689 ptime
->parseopts
= 0;
690 /* ptime->tmspec = ? */