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: src/usr.sbin/newsyslog/ptimes.c,v 1.5 2004/06/07 21:53:27 gad Exp $");
45
46#include <ctype.h>
47#include <limits.h>
48#include <stdio.h>
49#include <stdlib.h>
50#include <string.h>
51#include <time.h>
52
53#include "extern.h"
54
55#define	SECS_PER_HOUR	3600
56
57/*
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.
61 */
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
68
69#define	TNYET_ADJ4DST		-10	/* DST has "not yet" been adjusted */
70
71struct ptime_data {
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 */
80};
81
82static int	 days_pmonth(int month, int year);
83static int	 parse8601(struct ptime_data *ptime, const char *str);
84static int	 parseDWM(struct ptime_data *ptime, const char *str);
85
86/*
87 * Simple routine to calculate the number of days in a given month.
88 */
89static int
90days_pmonth(int month, int year)
91{
92	static const int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31,
93	    30, 31, 30, 31};
94	int ndays;
95
96	ndays = mtab[month];
97
98	if (month == 1) {
99		/*
100		 * We are usually called with a 'tm-year' value
101		 * (ie, the value = the number of years past 1900).
102		 */
103		if (year < 1900)
104			year += 1900;
105		if (year % 4 == 0) {
106			/*
107			 * This is a leap year, as long as it is not a
108			 * multiple of 100, or if it is a multiple of
109			 * both 100 and 400.
110			 */
111			if (year % 100 != 0)
112				ndays++;	/* not multiple of 100 */
113			else if (year % 400 == 0)
114				ndays++;	/* is multiple of 100 and 400 */
115		}
116	}
117	return (ndays);
118}
119
120/*-
121 * Parse a limited subset of ISO 8601. The specific format is as follows:
122 *
123 * [CC[YY[MM[DD]]]][THH[MM[SS]]]	(where `T' is the literal letter)
124 *
125 * We don't accept a timezone specification; missing fields (including timezone)
126 * are defaulted to the current date but time zero.
127 */
128static int
129parse8601(struct ptime_data *ptime, const char *s)
130{
131	char *t;
132	long l;
133	struct tm tm;
134
135	l = strtol(s, &t, 10);
136	if (l < 0 || l >= INT_MAX || (*t != '\0' && *t != 'T'))
137		return (-1);
138
139	/*
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.
143	 */
144	tm = ptime->tm;
145	ptime->tmspec = TSPEC_HOUROFDAY;
146	switch (t - s) {
147	case 8:
148		tm.tm_year = ((l / 1000000) - 19) * 100;
149		l = l % 1000000;
150	case 6:
151		ptime->tmspec |= TSPEC_YEAR;
152		tm.tm_year -= tm.tm_year % 100;
153		tm.tm_year += l / 10000;
154		l = l % 10000;
155	case 4:
156		ptime->tmspec |= TSPEC_MONTHOFYEAR;
157		tm.tm_mon = (l / 100) - 1;
158		l = l % 100;
159	case 2:
160		ptime->tmspec |= TSPEC_DAYOFMONTH;
161		tm.tm_mday = l;
162	case 0:
163		break;
164	default:
165		return (-1);
166	}
167
168	/* sanity check */
169	if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12
170	    || tm.tm_mday < 1 || tm.tm_mday > 31)
171		return (-1);
172
173	if (*t != '\0') {
174		s = ++t;
175		l = strtol(s, &t, 10);
176		if (l < 0 || l >= INT_MAX || (*t != '\0' && !isspace(*t)))
177			return (-1);
178
179		switch (t - s) {
180		case 6:
181			tm.tm_sec = l % 100;
182			l /= 100;
183		case 4:
184			tm.tm_min = l % 100;
185			l /= 100;
186		case 2:
187			ptime->tmspec |= TSPEC_HOUROFDAY;
188			tm.tm_hour = l;
189		case 0:
190			break;
191		default:
192			return (-1);
193		}
194
195		/* sanity check */
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)
198			return (-1);
199	}
200
201	ptime->tm = tm;
202	return (0);
203}
204
205/*-
206 * Parse a cyclic time specification, the format is as follows:
207 *
208 *	[Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
209 *
210 * to rotate a logfile cyclic at
211 *
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)
215 *
216 * We don't accept a timezone specification; missing fields
217 * are defaulted to the current date but time zero.
218 */
219static int
220parseDWM(struct ptime_data *ptime, const char *s)
221{
222	int daysmon, Dseen, WMseen;
223	const char *endval;
224	char *tmp;
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		endval = NULL;
236		switch (*s) {
237		case 'D':
238			if (Dseen)
239				return (-1);
240			Dseen++;
241			ptime->tmspec |= TSPEC_HOUROFDAY;
242			s++;
243			l = strtol(s, &tmp, 10);
244			if (l < 0 || l > 23)
245				return (-1);
246			endval = tmp;
247			tm.tm_hour = l;
248			break;
249
250		case 'W':
251			if (WMseen)
252				return (-1);
253			WMseen++;
254			ptime->tmspec |= TSPEC_DAYOFWEEK;
255			s++;
256			l = strtol(s, &tmp, 10);
257			if (l < 0 || l > 6)
258				return (-1);
259			endval = tmp;
260			if (l != tm.tm_wday) {
261				int save;
262
263				if (l < tm.tm_wday) {
264					save = 6 - tm.tm_wday;
265					save += (l + 1);
266				} else {
267					save = l - tm.tm_wday;
268				}
269
270				tm.tm_mday += save;
271
272				if (tm.tm_mday > daysmon) {
273					tm.tm_mon++;
274					tm.tm_mday = tm.tm_mday - daysmon;
275				}
276			}
277			break;
278
279		case 'M':
280			if (WMseen)
281				return (-1);
282			WMseen++;
283			ptime->tmspec |= TSPEC_DAYOFMONTH;
284			s++;
285			if (tolower(*s) == 'l') {
286				/* User wants the last day of the month. */
287				ptime->tmspec |= TSPEC_LDAYOFMONTH;
288				tm.tm_mday = daysmon;
289				endval = s + 1;
290			} else {
291				l = strtol(s, &tmp, 10);
292				if (l < 1 || l > 31)
293					return (-1);
294
295				if (l > daysmon)
296					return (-1);
297				endval = tmp;
298				tm.tm_mday = l;
299			}
300			break;
301
302		default:
303			return (-1);
304			break;
305		}
306
307		if (endval == NULL)
308			return (-1);
309		else if (*endval == '\0' || isspace(*endval))
310			break;
311		else
312			s = endval;
313	}
314
315	ptime->tm = tm;
316	return (0);
317}
318
319/*
320 * Initialize a new ptime-related data area.
321 */
322struct ptime_data *
323ptime_init(const struct ptime_data *optsrc)
324{
325	struct ptime_data *newdata;
326
327	newdata = malloc(sizeof(struct ptime_data));
328	if (optsrc != NULL) {
329		memcpy(newdata, optsrc, sizeof(struct ptime_data));
330	} else {
331		memset(newdata, '\0', sizeof(struct ptime_data));
332		newdata->did_adj4dst = TNYET_ADJ4DST;
333	}
334
335	return (newdata);
336}
337
338/*
339 * Adjust a given time if that time is in a different timezone than
340 * some other time.
341 */
342int
343ptime_adjust4dst(struct ptime_data *ptime, const struct ptime_data *dstsrc)
344{
345	struct ptime_data adjtime;
346
347	if (ptime == NULL)
348		return (-1);
349
350	/*
351	 * Changes are not made to the given time until after all
352	 * of the calculations have been successful.
353	 */
354	adjtime = *ptime;
355
356	/* Check to see if this adjustment was already made */
357	if ((adjtime.did_adj4dst != TNYET_ADJ4DST) &&
358	    (adjtime.did_adj4dst == dstsrc->tm.tm_isdst))
359		return (0);		/* yes, so don't make it twice */
360
361	/* See if daylight-saving has changed between the two times. */
362	if (dstsrc->tm.tm_isdst != adjtime.tm.tm_isdst) {
363		if (adjtime.tm.tm_isdst == 1)
364			adjtime.tsecs -= SECS_PER_HOUR;
365		else if (adjtime.tm.tm_isdst == 0)
366			adjtime.tsecs += SECS_PER_HOUR;
367		adjtime.tm = *(localtime(&adjtime.tsecs));
368		/* Remember that this adjustment has been made */
369		adjtime.did_adj4dst = dstsrc->tm.tm_isdst;
370		/*
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?
374		 */
375	}
376
377	*ptime = adjtime;
378	return (0);
379}
380
381int
382ptime_relparse(struct ptime_data *ptime, int parseopts, time_t basetime,
383    const char *str)
384{
385	int dpm, pres;
386	struct tm temp_tm;
387
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;
393
394	/*
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.
398	 */
399	if (parseopts & PTM_PARSE_DWM)
400		pres = parseDWM(ptime, str);
401	else
402		pres = parse8601(ptime, str);
403	if (pres < 0) {
404		ptime->tsecs = (time_t)pres;
405		return (pres);
406	}
407
408	/*
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).
414	 */
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)) {
419		/*
420		 * ptime_nxtime() will want a ptime->tsecs value,
421		 * but we need to avoid mktime resetting all the
422		 * ptime->tm values.
423		 */
424		if (verbose && dbg_at_times > 1)
425			fprintf(stderr,
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);
430		temp_tm = ptime->tm;
431		ptime->tsecs = mktime(&temp_tm);
432		if (ptime->tsecs > (time_t)-1)
433			ptimeset_nxtime(ptime);
434		if (verbose && dbg_at_times > 1)
435			fprintf(stderr,
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,
439			    ptime->tm.tm_min);
440	}
441
442	/*
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".
446	 */
447	ptime->tsecs = mktime(&ptime->tm);
448	if (ptime->tsecs == (time_t)-1) {
449		ptime->tsecs = (time_t)-2;
450		return (-2);
451	}
452
453	return (0);
454}
455
456int
457ptime_free(struct ptime_data *ptime)
458{
459
460	if (ptime == NULL)
461		return (-1);
462
463	free(ptime);
464	return (0);
465}
466
467/*
468 * Some trivial routines so ptime_data can remain a completely
469 * opaque type.
470 */
471const char *
472ptimeget_ctime(const struct ptime_data *ptime)
473{
474
475	if (ptime == NULL)
476		return ("Null time in ptimeget_ctime()\n");
477
478	return (ctime(&ptime->tsecs));
479}
480
481double
482ptimeget_diff(const struct ptime_data *minuend, const struct
483    ptime_data *subtrahend)
484{
485
486	/* Just like difftime(), we have no good error-return */
487	if (minuend == NULL || subtrahend == NULL)
488		return (0.0);
489
490	return (difftime(minuend->tsecs, subtrahend->tsecs));
491}
492
493time_t
494ptimeget_secs(const struct ptime_data *ptime)
495{
496
497	if (ptime == NULL)
498		return (-1);
499
500	return (ptime->tsecs);
501}
502
503/*
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.
508 */
509int
510ptimeset_nxtime(struct ptime_data *ptime)
511{
512	int moredays, tdpm, tmon, tyear;
513	struct ptime_data nextmatch;
514
515	if (ptime == NULL)
516		return (-1);
517
518	/*
519	 * Changes are not made to the given time until after all
520	 * of the calculations have been successful.
521	 */
522	nextmatch = *ptime;
523	/*
524	 * If the user specified a year and we're already past that
525	 * time, then there will never be another one!
526	 */
527	if (ptime->tmspec & TSPEC_YEAR)
528		return (-1);
529
530	/*
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.
535	 */
536	moredays = 0;
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;
543		} else {
544			nextmatch.tm.tm_year += 1;
545		}
546		nextmatch.tm.tm_isdst = -1;
547		nextmatch.tsecs = mktime(&nextmatch.tm);
548
549	} else if (ptime->tmspec & TSPEC_LDAYOFMONTH) {
550		/*
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.
554		 */
555		if (ptime->tm.tm_mon < 11)
556			moredays = days_pmonth(ptime->tm.tm_mon + 1,
557			    ptime->tm.tm_year);
558		else
559			moredays = days_pmonth(0, ptime->tm.tm_year + 1);
560
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);
564		/*
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.
568		 */
569		tmon = ptime->tm.tm_mon;
570		tyear = ptime->tm.tm_year;
571		for (;;) {
572			if (tmon < 11)
573				tmon += 1;
574			else {
575				tmon = 0;
576				tyear += 1;
577			}
578			tdpm = days_pmonth(tmon, tyear);
579			if (tdpm >= ptime->tm.tm_mday)
580				break;
581			moredays += tdpm;
582		}
583
584	} else if (ptime->tmspec & TSPEC_DAYOFWEEK) {
585		moredays = 7;
586	} else if (ptime->tmspec & TSPEC_HOUROFDAY) {
587		moredays = 1;
588	}
589
590	if (moredays != 0) {
591		nextmatch.tsecs += SECS_PER_HOUR * 24 * moredays;
592		nextmatch.tm = *(localtime(&nextmatch.tsecs));
593	}
594
595	/*
596	 * The new time will need to be adjusted if the setting of
597	 * daylight-saving has changed between the two times.
598	 */
599	ptime_adjust4dst(&nextmatch, ptime);
600
601	/* Everything worked.  Update the given time and return. */
602	*ptime = nextmatch;
603	return (0);
604}
605
606int
607ptimeset_time(struct ptime_data *ptime, time_t secs)
608{
609
610	if (ptime == NULL)
611		return (-1);
612
613	ptime->tsecs = secs;
614	ptime->tm = *(localtime(&ptime->tsecs));
615	ptime->parseopts = 0;
616	/* ptime->tmspec = ? */
617	return (0);
618}
619