xref: /freebsd/usr.sbin/newsyslog/ptimes.c (revision 2357939bc239bd5334a169b62313806178dd8f30)
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