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;
150 ptime
->tmspec
|= TSPEC_YEAR
;
151 tm
.tm_year
-= tm
.tm_year
% 100;
152 tm
.tm_year
+= l
/ 10000;
155 ptime
->tmspec
|= TSPEC_MONTHOFYEAR
;
156 tm
.tm_mon
= (l
/ 100) - 1;
159 ptime
->tmspec
|= TSPEC_DAYOFMONTH
;
168 if (tm
.tm_year
< 70 || tm
.tm_mon
< 0 || tm
.tm_mon
> 12
169 || tm
.tm_mday
< 1 || tm
.tm_mday
> 31)
174 l
= strtol(s
, &t
, 10);
175 if (l
< 0 || l
>= INT_MAX
|| (*t
!= '\0' && !isspace(*t
)))
186 ptime
->tmspec
|= TSPEC_HOUROFDAY
;
195 if (tm
.tm_sec
< 0 || tm
.tm_sec
> 60 || tm
.tm_min
< 0
196 || tm
.tm_min
> 59 || tm
.tm_hour
< 0 || tm
.tm_hour
> 23)
205 * Parse a cyclic time specification, the format is as follows:
207 * [Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
209 * to rotate a logfile cyclic at
211 * - every day (D) within a specific hour (hh) (hh = 0...23)
212 * - once a week (W) at a specific day (d) OR (d = 0..6, 0 = Sunday)
213 * - once a month (M) at a specific day (d) (d = 1..31,l|L)
215 * We don't accept a timezone specification; missing fields
216 * are defaulted to the current date but time zero.
219 parseDWM(struct ptime_data
*ptime
, const char *s
)
221 int daysmon
, Dseen
, WMseen
;
227 /* Save away the number of days in this month */
229 daysmon
= days_pmonth(tm
.tm_mon
, tm
.tm_year
);
232 ptime
->tmspec
= TSPEC_HOUROFDAY
;
240 ptime
->tmspec
|= TSPEC_HOUROFDAY
;
242 l
= strtol(s
, &tmp
, 10);
253 ptime
->tmspec
|= TSPEC_DAYOFWEEK
;
255 l
= strtol(s
, &tmp
, 10);
259 if (l
!= tm
.tm_wday
) {
262 if (l
< tm
.tm_wday
) {
263 save
= 6 - tm
.tm_wday
;
266 save
= l
- tm
.tm_wday
;
271 if (tm
.tm_mday
> daysmon
) {
273 tm
.tm_mday
= tm
.tm_mday
- daysmon
;
282 ptime
->tmspec
|= TSPEC_DAYOFMONTH
;
284 if (tolower(*s
) == 'l') {
285 /* User wants the last day of the month. */
286 ptime
->tmspec
|= TSPEC_LDAYOFMONTH
;
287 tm
.tm_mday
= daysmon
;
290 l
= strtol(s
, &tmp
, 10);
308 else if (*endval
== '\0' || isspace(*endval
))
319 * Initialize a new ptime-related data area.
322 ptime_init(const struct ptime_data
*optsrc
)
324 struct ptime_data
*newdata
;
326 newdata
= malloc(sizeof(struct ptime_data
));
327 if (optsrc
!= NULL
) {
328 memcpy(newdata
, optsrc
, sizeof(struct ptime_data
));
330 memset(newdata
, '\0', sizeof(struct ptime_data
));
331 newdata
->did_adj4dst
= TNYET_ADJ4DST
;
338 * Adjust a given time if that time is in a different timezone than
342 ptime_adjust4dst(struct ptime_data
*ptime
, const struct ptime_data
*dstsrc
)
344 struct ptime_data adjtime
;
350 * Changes are not made to the given time until after all
351 * of the calculations have been successful.
355 /* Check to see if this adjustment was already made */
356 if ((adjtime
.did_adj4dst
!= TNYET_ADJ4DST
) &&
357 (adjtime
.did_adj4dst
== dstsrc
->tm
.tm_isdst
))
358 return (0); /* yes, so don't make it twice */
360 /* See if daylight-saving has changed between the two times. */
361 if (dstsrc
->tm
.tm_isdst
!= adjtime
.tm
.tm_isdst
) {
362 if (adjtime
.tm
.tm_isdst
== 1)
363 adjtime
.tsecs
-= SECS_PER_HOUR
;
364 else if (adjtime
.tm
.tm_isdst
== 0)
365 adjtime
.tsecs
+= SECS_PER_HOUR
;
366 adjtime
.tm
= *(localtime(&adjtime
.tsecs
));
367 /* Remember that this adjustment has been made */
368 adjtime
.did_adj4dst
= dstsrc
->tm
.tm_isdst
;
370 * XXX - Should probably check to see if changing the
371 * hour also changed the value of is_dst. What
372 * should we do in that case?
381 ptime_relparse(struct ptime_data
*ptime
, int parseopts
, time_t basetime
,
387 ptime
->parseopts
= parseopts
;
388 ptime
->basesecs
= basetime
;
389 ptime
->basetm
= *(localtime(&ptime
->basesecs
));
390 ptime
->tm
= ptime
->basetm
;
391 ptime
->tm
.tm_hour
= ptime
->tm
.tm_min
= ptime
->tm
.tm_sec
= 0;
394 * Call a routine which sets ptime.tm and ptime.tspecs based
395 * on the given string and parsing-options. Note that the
396 * routine should not call mktime to set ptime.tsecs.
398 if (parseopts
& PTM_PARSE_DWM
)
399 pres
= parseDWM(ptime
, str
);
401 pres
= parse8601(ptime
, str
);
403 ptime
->tsecs
= (time_t)pres
;
408 * Before calling mktime, check to see if we ended up with a
409 * "day-of-month" that does not exist in the selected month.
410 * If we did call mktime with that info, then mktime will
411 * make it look like the user specifically requested a day
412 * in the following month (eg: Feb 31 turns into Mar 3rd).
414 dpm
= days_pmonth(ptime
->tm
.tm_mon
, ptime
->tm
.tm_year
);
415 if ((parseopts
& PTM_PARSE_MATCHDOM
) &&
416 (ptime
->tmspec
& TSPEC_DAYOFMONTH
) &&
417 (ptime
->tm
.tm_mday
> dpm
)) {
419 * ptime_nxtime() will want a ptime->tsecs value,
420 * but we need to avoid mktime resetting all the
423 if (verbose
&& dbg_at_times
> 1)
425 "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)",
426 ptime
->tm
.tm_year
, ptime
->tm
.tm_mon
,
427 ptime
->tm
.tm_mday
, ptime
->tm
.tm_hour
,
428 ptime
->tm
.tm_min
, dpm
);
430 ptime
->tsecs
= mktime(&temp_tm
);
431 if (ptime
->tsecs
> (time_t)-1)
432 ptimeset_nxtime(ptime
);
433 if (verbose
&& dbg_at_times
> 1)
435 " to: %4d/%02d/%02d %02d:%02d\n",
436 ptime
->tm
.tm_year
, ptime
->tm
.tm_mon
,
437 ptime
->tm
.tm_mday
, ptime
->tm
.tm_hour
,
442 * Convert the ptime.tm into standard time_t seconds. Check
443 * for invalid times, which includes things like the hour lost
444 * when switching from "standard time" to "daylight saving".
446 ptime
->tsecs
= mktime(&ptime
->tm
);
447 if (ptime
->tsecs
== (time_t)-1) {
448 ptime
->tsecs
= (time_t)-2;
456 ptime_free(struct ptime_data
*ptime
)
467 * Some trivial routines so ptime_data can remain a completely
471 ptimeget_ctime(const struct ptime_data
*ptime
)
475 return ("Null time in ptimeget_ctime()\n");
477 return (ctime(&ptime
->tsecs
));
481 * Generate a time of day string in an RFC5424 compatible format. Return a
482 * pointer to the buffer with the timestamp string or NULL if an error. If the
483 * time is not supplied, cannot be converted to local time, or the resulting
484 * string would overflow the buffer, the returned string will be the RFC5424
488 ptimeget_ctime_rfc5424(const struct ptime_data
*ptime
,
489 char *timebuf
, size_t bufsize
)
491 static const char NILVALUE
[] = {"-"}; /* RFC5424 specified NILVALUE */
499 if (timebuf
== NULL
) {
503 if (bufsize
< sizeof(NILVALUE
)) {
508 * Convert to localtime. RFC5424 mandates the use of the NILVALUE if
509 * the time cannot be obtained, so use that if there is an error in the
512 if (ptime
== NULL
|| localtime_r(&(ptime
->tsecs
), &tm
) == NULL
) {
513 strlcpy(timebuf
, NILVALUE
, bufsize
);
518 * Convert the time to a string in RFC5424 format. The conversion
519 * cannot be done with strftime() because it cannot produce the correct
520 * timezone offset format.
522 if (tm
.tm_gmtoff
< 0) {
524 tz_offset
= -tm
.tm_gmtoff
;
527 tz_offset
= tm
.tm_gmtoff
;
530 tz_hours
= tz_offset
/ 3600;
531 tz_mins
= (tz_offset
% 3600) / 60;
533 chars
= snprintf(timebuf
, bufsize
,
534 "%04d-%02d-%02d" /* date */
535 "T%02d:%02d:%02d" /* time */
536 "%c%02d:%02d", /* time zone offset */
537 tm
.tm_year
+ 1900, tm
.tm_mon
+ 1, tm
.tm_mday
,
538 tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
,
539 tz_sign
, tz_hours
, tz_mins
);
541 /* If the timestamp is too big for timebuf, return the NILVALUE. */
542 if (chars
>= (int)bufsize
) {
543 strlcpy(timebuf
, NILVALUE
, bufsize
);
550 ptimeget_diff(const struct ptime_data
*minuend
, const struct
551 ptime_data
*subtrahend
)
554 /* Just like difftime(), we have no good error-return */
555 if (minuend
== NULL
|| subtrahend
== NULL
)
558 return (difftime(minuend
->tsecs
, subtrahend
->tsecs
));
562 ptimeget_secs(const struct ptime_data
*ptime
)
568 return (ptime
->tsecs
);
572 * Generate an approximate timestamp for the next event, based on
573 * what parts of time were specified by the original parameter to
574 * ptime_relparse(). The result may be -1 if there is no obvious
575 * "next time" which will work.
578 ptimeset_nxtime(struct ptime_data
*ptime
)
580 int moredays
, tdpm
, tmon
, tyear
;
581 struct ptime_data nextmatch
;
587 * Changes are not made to the given time until after all
588 * of the calculations have been successful.
592 * If the user specified a year and we're already past that
593 * time, then there will never be another one!
595 if (ptime
->tmspec
& TSPEC_YEAR
)
599 * The caller gave us a time in the past. Calculate how much
600 * time is needed to go from that valid rotate time to the
601 * next valid rotate time. We only need to get to the nearest
602 * hour, because newsyslog is only run once per hour.
605 if (ptime
->tmspec
& TSPEC_MONTHOFYEAR
) {
606 /* Special case: Feb 29th does not happen every year. */
607 if (ptime
->tm
.tm_mon
== 1 && ptime
->tm
.tm_mday
== 29) {
608 nextmatch
.tm
.tm_year
+= 4;
609 if (days_pmonth(1, nextmatch
.tm
.tm_year
) < 29)
610 nextmatch
.tm
.tm_year
+= 4;
612 nextmatch
.tm
.tm_year
+= 1;
614 nextmatch
.tm
.tm_isdst
= -1;
615 nextmatch
.tsecs
= mktime(&nextmatch
.tm
);
617 } else if (ptime
->tmspec
& TSPEC_LDAYOFMONTH
) {
619 * Need to get to the last day of next month. Origtm is
620 * already at the last day of this month, so just add to
621 * it number of days in the next month.
623 if (ptime
->tm
.tm_mon
< 11)
624 moredays
= days_pmonth(ptime
->tm
.tm_mon
+ 1,
627 moredays
= days_pmonth(0, ptime
->tm
.tm_year
+ 1);
629 } else if (ptime
->tmspec
& TSPEC_DAYOFMONTH
) {
630 /* Jump to the same day in the next month */
631 moredays
= days_pmonth(ptime
->tm
.tm_mon
, ptime
->tm
.tm_year
);
633 * In some cases, the next month may not *have* the
634 * desired day-of-the-month. If that happens, then
635 * move to the next month that does have enough days.
637 tmon
= ptime
->tm
.tm_mon
;
638 tyear
= ptime
->tm
.tm_year
;
646 tdpm
= days_pmonth(tmon
, tyear
);
647 if (tdpm
>= ptime
->tm
.tm_mday
)
652 } else if (ptime
->tmspec
& TSPEC_DAYOFWEEK
) {
654 } else if (ptime
->tmspec
& TSPEC_HOUROFDAY
) {
659 nextmatch
.tsecs
+= SECS_PER_HOUR
* 24 * moredays
;
660 nextmatch
.tm
= *(localtime(&nextmatch
.tsecs
));
664 * The new time will need to be adjusted if the setting of
665 * daylight-saving has changed between the two times.
667 ptime_adjust4dst(&nextmatch
, ptime
);
669 /* Everything worked. Update the given time and return. */
675 ptimeset_time(struct ptime_data
*ptime
, time_t secs
)
682 ptime
->tm
= *(localtime(&ptime
->tsecs
));
683 ptime
->parseopts
= 0;
684 /* ptime->tmspec = ? */