ptimes.c revision 120361
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: head/usr.sbin/newsyslog/ptimes.c 120361 2003-09-23 00:00:26Z gad $");
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
72struct 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
83static int	 days_pmonth(int month, int year);
84static int	 parse8601(struct ptime_data *ptime, const char *str);
85static 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 */
90static int
91days_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 */
129static int
130parse8601(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 */
220static int
221parseDWM(struct ptime_data *ptime, const char *s)
222{
223	int daysmon, Dseen, WMseen;
224	char *t;
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		switch (*s) {
236		case 'D':
237			if (Dseen)
238				return (-1);
239			Dseen++;
240			ptime->tmspec |= TSPEC_HOUROFDAY;
241			s++;
242			l = strtol(s, &t, 10);
243			if (l < 0 || l > 23)
244				return (-1);
245			tm.tm_hour = l;
246			break;
247
248		case 'W':
249			if (WMseen)
250				return (-1);
251			WMseen++;
252			ptime->tmspec |= TSPEC_DAYOFWEEK;
253			s++;
254			l = strtol(s, &t, 10);
255			if (l < 0 || l > 6)
256				return (-1);
257			if (l != tm.tm_wday) {
258				int save;
259
260				if (l < tm.tm_wday) {
261					save = 6 - tm.tm_wday;
262					save += (l + 1);
263				} else {
264					save = l - tm.tm_wday;
265				}
266
267				tm.tm_mday += save;
268
269				if (tm.tm_mday > daysmon) {
270					tm.tm_mon++;
271					tm.tm_mday = tm.tm_mday - daysmon;
272				}
273			}
274			break;
275
276		case 'M':
277			if (WMseen)
278				return (-1);
279			WMseen++;
280			ptime->tmspec |= TSPEC_DAYOFMONTH;
281			s++;
282			if (tolower(*s) == 'l') {
283				/* User wants the last day of the month. */
284				ptime->tmspec |= TSPEC_LDAYOFMONTH;
285				tm.tm_mday = daysmon;
286				s++;
287				t = __DECONST(char *,s);
288			} else {
289				l = strtol(s, &t, 10);
290				if (l < 1 || l > 31)
291					return (-1);
292
293				if (l > daysmon)
294					return (-1);
295				tm.tm_mday = l;
296			}
297			break;
298
299		default:
300			return (-1);
301			break;
302		}
303
304		if (*t == '\0' || isspace(*t))
305			break;
306		else
307			s = t;
308	}
309
310	ptime->tm = tm;
311	return (0);
312}
313
314/*
315 * Initialize a new ptime-related data area.
316 */
317struct ptime_data *
318ptime_init(const struct ptime_data *optsrc)
319{
320	struct ptime_data *newdata;
321
322	newdata = malloc(sizeof(struct ptime_data));
323	if (optsrc != NULL) {
324		memcpy(newdata, optsrc, sizeof(struct ptime_data));
325	} else {
326		memset(newdata, '\0', sizeof(struct ptime_data));
327		newdata->did_adj4dst = TNYET_ADJ4DST;
328	}
329
330	return (newdata);
331}
332
333/*
334 * Adjust a given time if that time is in a different timezone than
335 * some other time.
336 */
337int
338ptime_adjust4dst(struct ptime_data *ptime, const struct ptime_data *dstsrc)
339{
340	struct ptime_data adjtime;
341
342	if (ptime == NULL)
343		return (-1);
344
345	/*
346	 * Changes are not made to the given time until after all
347	 * of the calculations have been successful.
348	 */
349	adjtime = *ptime;
350
351	/* Check to see if this adjustment was already made */
352	if ((adjtime.did_adj4dst != TNYET_ADJ4DST) &&
353	    (adjtime.did_adj4dst == dstsrc->tm.tm_isdst))
354		return (0);		/* yes, so don't make it twice */
355
356	/* See if daylight-saving has changed between the two times. */
357	if (dstsrc->tm.tm_isdst != adjtime.tm.tm_isdst) {
358		if (adjtime.tm.tm_isdst == 1)
359			adjtime.tsecs -= SECS_PER_HOUR;
360		else if (adjtime.tm.tm_isdst == 0)
361			adjtime.tsecs += SECS_PER_HOUR;
362		adjtime.tm = *(localtime(&adjtime.tsecs));
363		/* Remember that this adjustment has been made */
364		adjtime.did_adj4dst = dstsrc->tm.tm_isdst;
365		/*
366		 * XXX - Should probably check to see if changing the
367		 *	hour also changed the value of is_dst.  What
368		 *	should we do in that case?
369		 */
370	}
371
372	*ptime = adjtime;
373	return (0);
374}
375
376int
377ptime_relparse(struct ptime_data *ptime, int parseopts, time_t basetime,
378    const char *str)
379{
380	int dpm, pres;
381	struct tm temp_tm;
382
383	ptime->parseopts = parseopts;
384	ptime->basesecs = basetime;
385	ptime->basetm = *(localtime(&ptime->basesecs));
386	ptime->tm = ptime->basetm;
387	ptime->tm.tm_hour = ptime->tm.tm_min = ptime->tm.tm_sec = 0;
388
389	/*
390	 * Call a routine which sets ptime.tm and ptime.tspecs based
391	 * on the given string and parsing-options.  Note that the
392	 * routine should not call mktime to set ptime.tsecs.
393	 */
394	if (parseopts & PTM_PARSE_DWM)
395		pres = parseDWM(ptime, str);
396	else
397		pres = parse8601(ptime, str);
398	if (pres < 0) {
399		ptime->tsecs = (time_t)pres;
400		return (pres);
401	}
402
403	/*
404	 * Before calling mktime, check to see if we ended up with a
405	 * "day-of-month" that does not exist in the selected month.
406	 * If we did call mktime with that info, then mktime will
407	 * make it look like the user specifically requested a day
408	 * in the following month (eg: Feb 31 turns into Mar 3rd).
409	 */
410	dpm = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year);
411	if ((parseopts & PTM_PARSE_MATCHDOM) &&
412	    (ptime->tmspec & TSPEC_DAYOFMONTH) &&
413	    (ptime->tm.tm_mday> dpm)) {
414		/*
415		 * ptime_nxtime() will want a ptime->tsecs value,
416		 * but we need to avoid mktime resetting all the
417		 * ptime->tm values.
418		 */
419		if (verbose && dbg_at_times > 1)
420			fprintf(stderr,
421			    "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)",
422			    ptime->tm.tm_year, ptime->tm.tm_mon,
423			    ptime->tm.tm_mday, ptime->tm.tm_hour,
424			    ptime->tm.tm_min, dpm);
425		temp_tm = ptime->tm;
426		ptime->tsecs = mktime(&temp_tm);
427		if (ptime->tsecs > (time_t)-1)
428			ptimeset_nxtime(ptime);
429		if (verbose && dbg_at_times > 1)
430			fprintf(stderr,
431			    " to: %4d/%02d/%02d %02d:%02d\n",
432			    ptime->tm.tm_year, ptime->tm.tm_mon,
433			    ptime->tm.tm_mday, ptime->tm.tm_hour,
434			    ptime->tm.tm_min);
435	}
436
437	/*
438	 * Convert the ptime.tm into standard time_t seconds.  Check
439	 * for invalid times, which includes things like the hour lost
440	 * when switching from "standard time" to "daylight saving".
441	 */
442	ptime->tsecs = mktime(&ptime->tm);
443	if (ptime->tsecs == (time_t)-1) {
444		ptime->tsecs = (time_t)-2;
445		return (-2);
446	}
447
448	return (0);
449}
450
451int
452ptime_free(struct ptime_data *ptime)
453{
454
455	if (ptime == NULL)
456		return (-1);
457
458	free(ptime);
459	return (0);
460}
461
462/*
463 * Some trivial routines so ptime_data can remain a completely
464 * opaque type.
465 */
466const char *
467ptimeget_ctime(const struct ptime_data *ptime)
468{
469
470	if (ptime == NULL)
471		return ("Null time in ptimeget_ctime()\n");
472
473	return (ctime(&ptime->tsecs));
474}
475
476double
477ptimeget_diff(const struct ptime_data *minuend, const struct
478    ptime_data *subtrahend)
479{
480
481	/* Just like difftime(), we have no good error-return */
482	if (minuend == NULL || subtrahend == NULL)
483		return (0.0);
484
485	return (difftime(minuend->tsecs, subtrahend->tsecs));
486}
487
488time_t
489ptimeget_secs(const struct ptime_data *ptime)
490{
491
492	if (ptime == NULL)
493		return (-1);
494
495	return (ptime->tsecs);
496}
497
498/*
499 * Generate an approximate timestamp for the next event, based on
500 * what parts of time were specified by the original parameter to
501 * ptime_relparse(). The result may be -1 if there is no obvious
502 * "next time" which will work.
503 */
504int
505ptimeset_nxtime(struct ptime_data *ptime)
506{
507	int moredays, tdpm, tmon, tyear;
508	struct ptime_data nextmatch;
509
510	if (ptime == NULL)
511		return (-1);
512
513	/*
514	 * Changes are not made to the given time until after all
515	 * of the calculations have been successful.
516	 */
517	nextmatch = *ptime;
518	/*
519	 * If the user specified a year and we're already past that
520	 * time, then there will never be another one!
521	 */
522	if (ptime->tmspec & TSPEC_YEAR)
523		return (-1);
524
525	/*
526	 * The caller gave us a time in the past.  Calculate how much
527	 * time is needed to go from that valid rotate time to the
528	 * next valid rotate time.  We only need to get to the nearest
529	 * hour, because newsyslog is only run once per hour.
530	 */
531	moredays = 0;
532	if (ptime->tmspec & TSPEC_MONTHOFYEAR) {
533		/* Special case: Feb 29th does not happen every year. */
534		if (ptime->tm.tm_mon == 1 && ptime->tm.tm_mday == 29) {
535			nextmatch.tm.tm_year += 4;
536			if (days_pmonth(1, nextmatch.tm.tm_year) < 29)
537				nextmatch.tm.tm_year += 4;
538		} else {
539			nextmatch.tm.tm_year += 1;
540		}
541		nextmatch.tm.tm_isdst = -1;
542		nextmatch.tsecs = mktime(&nextmatch.tm);
543
544	} else if (ptime->tmspec & TSPEC_LDAYOFMONTH) {
545		/*
546		 * Need to get to the last day of next month.  Origtm is
547		 * already at the last day of this month, so just add to
548		 * it number of days in the next month.
549		 */
550		if (ptime->tm.tm_mon < 11)
551			moredays = days_pmonth(ptime->tm.tm_mon + 1,
552			    ptime->tm.tm_year);
553		else
554			moredays = days_pmonth(0, ptime->tm.tm_year + 1);
555
556	} else if (ptime->tmspec & TSPEC_DAYOFMONTH) {
557		/* Jump to the same day in the next month */
558		moredays = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year);
559		/*
560		 * In some cases, the next month may not *have* the
561		 * desired day-of-the-month.  If that happens, then
562		 * move to the next month that does have enough days.
563		 */
564		tmon = ptime->tm.tm_mon;
565		tyear = ptime->tm.tm_year;
566		for (;;) {
567			if (tmon < 11)
568				tmon += 1;
569			else {
570				tmon = 0;
571				tyear += 1;
572			}
573			tdpm = days_pmonth(tmon, tyear);
574			if (tdpm >= ptime->tm.tm_mday)
575				break;
576			moredays += tdpm;
577		}
578
579	} else if (ptime->tmspec & TSPEC_DAYOFWEEK) {
580		moredays = 7;
581	} else if (ptime->tmspec & TSPEC_HOUROFDAY) {
582		moredays = 1;
583	}
584
585	if (moredays != 0) {
586		nextmatch.tsecs += SECS_PER_HOUR * 24 * moredays;
587		nextmatch.tm = *(localtime(&nextmatch.tsecs));
588	}
589
590	/*
591	 * The new time will need to be adjusted if the setting of
592	 * daylight-saving has changed between the two times.
593	 */
594	ptime_adjust4dst(&nextmatch, ptime);
595
596	/* Everything worked.  Update the given time and return. */
597	*ptime = nextmatch;
598	return (0);
599}
600
601int
602ptimeset_time(struct ptime_data *ptime, time_t secs)
603{
604
605	if (ptime == NULL)
606		return (-1);
607
608	ptime->tsecs = secs;
609	ptime->tm = *(localtime(&ptime->tsecs));
610	ptime->parseopts = 0;
611	/* ptime->tmspec = ? */
612	return (0);
613}
614