/*- * ------+---------+---------+---------+---------+---------+---------+---------* * Initial version of parse8601 was originally added to newsyslog.c in * FreeBSD on Jan 22, 1999 by Garrett Wollman . * Initial version of parseDWM was originally added to newsyslog.c in * FreeBSD on Apr 4, 2000 by Hellmuth Michaelis . * * Copyright (c) 2003 - Garance Alistair Drosehn . * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The views and conclusions contained in the software and documentation * are those of the authors and should not be interpreted as representing * official policies, either expressed or implied, of the FreeBSD Project. * * ------+---------+---------+---------+---------+---------+---------+---------* * This is intended to be a set of general-purpose routines to process times. * Right now it probably still has a number of assumptions in it, such that * it works fine for newsyslog but might not work for other uses. * ------+---------+---------+---------+---------+---------+---------+---------* */ #include __FBSDID("$FreeBSD: src/usr.sbin/newsyslog/ptimes.c,v 1.5 2004/06/07 21:53:27 gad Exp $"); #include #include #include #include #include #include #include "extern.h" #define SECS_PER_HOUR 3600 /* * Bit-values which indicate which components of time were specified * by the string given to parse8601 or parseDWM. These are needed to * calculate what time-in-the-future will match that string. */ #define TSPEC_YEAR 0x0001 #define TSPEC_MONTHOFYEAR 0x0002 #define TSPEC_LDAYOFMONTH 0x0004 #define TSPEC_DAYOFMONTH 0x0008 #define TSPEC_DAYOFWEEK 0x0010 #define TSPEC_HOUROFDAY 0x0020 #define TNYET_ADJ4DST -10 /* DST has "not yet" been adjusted */ struct ptime_data { time_t basesecs; /* Base point for relative times */ time_t tsecs; /* Time in seconds */ struct tm basetm; /* Base Time expanded into fields */ struct tm tm; /* Time expanded into fields */ int did_adj4dst; /* Track calls to ptime_adjust4dst */ int parseopts; /* Options given for parsing */ int tmspec; /* Indicates which time fields had * been specified by the user */ }; static int days_pmonth(int month, int year); static int parse8601(struct ptime_data *ptime, const char *str); static int parseDWM(struct ptime_data *ptime, const char *str); /* * Simple routine to calculate the number of days in a given month. */ static int days_pmonth(int month, int year) { static const int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int ndays; ndays = mtab[month]; if (month == 1) { /* * We are usually called with a 'tm-year' value * (ie, the value = the number of years past 1900). */ if (year < 1900) year += 1900; if (year % 4 == 0) { /* * This is a leap year, as long as it is not a * multiple of 100, or if it is a multiple of * both 100 and 400. */ if (year % 100 != 0) ndays++; /* not multiple of 100 */ else if (year % 400 == 0) ndays++; /* is multiple of 100 and 400 */ } } return (ndays); } /*- * Parse a limited subset of ISO 8601. The specific format is as follows: * * [CC[YY[MM[DD]]]][THH[MM[SS]]] (where `T' is the literal letter) * * We don't accept a timezone specification; missing fields (including timezone) * are defaulted to the current date but time zero. */ static int parse8601(struct ptime_data *ptime, const char *s) { char *t; long l; struct tm tm; l = strtol(s, &t, 10); if (l < 0 || l >= INT_MAX || (*t != '\0' && *t != 'T')) return (-1); /* * Now t points either to the end of the string (if no time was * provided) or to the letter `T' which separates date and time in * ISO 8601. The pointer arithmetic is the same for either case. */ tm = ptime->tm; ptime->tmspec = TSPEC_HOUROFDAY; switch (t - s) { case 8: tm.tm_year = ((l / 1000000) - 19) * 100; l = l % 1000000; case 6: ptime->tmspec |= TSPEC_YEAR; tm.tm_year -= tm.tm_year % 100; tm.tm_year += l / 10000; l = l % 10000; case 4: ptime->tmspec |= TSPEC_MONTHOFYEAR; tm.tm_mon = (l / 100) - 1; l = l % 100; case 2: ptime->tmspec |= TSPEC_DAYOFMONTH; tm.tm_mday = l; case 0: break; default: return (-1); } /* sanity check */ if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12 || tm.tm_mday < 1 || tm.tm_mday > 31) return (-1); if (*t != '\0') { s = ++t; l = strtol(s, &t, 10); if (l < 0 || l >= INT_MAX || (*t != '\0' && !isspace(*t))) return (-1); switch (t - s) { case 6: tm.tm_sec = l % 100; l /= 100; case 4: tm.tm_min = l % 100; l /= 100; case 2: ptime->tmspec |= TSPEC_HOUROFDAY; tm.tm_hour = l; case 0: break; default: return (-1); } /* sanity check */ if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0 || tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23) return (-1); } ptime->tm = tm; return (0); } /*- * Parse a cyclic time specification, the format is as follows: * * [Dhh] or [Wd[Dhh]] or [Mdd[Dhh]] * * to rotate a logfile cyclic at * * - every day (D) within a specific hour (hh) (hh = 0...23) * - once a week (W) at a specific day (d) OR (d = 0..6, 0 = Sunday) * - once a month (M) at a specific day (d) (d = 1..31,l|L) * * We don't accept a timezone specification; missing fields * are defaulted to the current date but time zero. */ static int parseDWM(struct ptime_data *ptime, const char *s) { int daysmon, Dseen, WMseen; const char *endval; char *tmp; long l; struct tm tm; /* Save away the number of days in this month */ tm = ptime->tm; daysmon = days_pmonth(tm.tm_mon, tm.tm_year); WMseen = Dseen = 0; ptime->tmspec = TSPEC_HOUROFDAY; for (;;) { endval = NULL; switch (*s) { case 'D': if (Dseen) return (-1); Dseen++; ptime->tmspec |= TSPEC_HOUROFDAY; s++; l = strtol(s, &tmp, 10); if (l < 0 || l > 23) return (-1); endval = tmp; tm.tm_hour = l; break; case 'W': if (WMseen) return (-1); WMseen++; ptime->tmspec |= TSPEC_DAYOFWEEK; s++; l = strtol(s, &tmp, 10); if (l < 0 || l > 6) return (-1); endval = tmp; if (l != tm.tm_wday) { int save; if (l < tm.tm_wday) { save = 6 - tm.tm_wday; save += (l + 1); } else { save = l - tm.tm_wday; } tm.tm_mday += save; if (tm.tm_mday > daysmon) { tm.tm_mon++; tm.tm_mday = tm.tm_mday - daysmon; } } break; case 'M': if (WMseen) return (-1); WMseen++; ptime->tmspec |= TSPEC_DAYOFMONTH; s++; if (tolower(*s) == 'l') { /* User wants the last day of the month. */ ptime->tmspec |= TSPEC_LDAYOFMONTH; tm.tm_mday = daysmon; endval = s + 1; } else { l = strtol(s, &tmp, 10); if (l < 1 || l > 31) return (-1); if (l > daysmon) return (-1); endval = tmp; tm.tm_mday = l; } break; default: return (-1); break; } if (endval == NULL) return (-1); else if (*endval == '\0' || isspace(*endval)) break; else s = endval; } ptime->tm = tm; return (0); } /* * Initialize a new ptime-related data area. */ struct ptime_data * ptime_init(const struct ptime_data *optsrc) { struct ptime_data *newdata; newdata = malloc(sizeof(struct ptime_data)); if (optsrc != NULL) { memcpy(newdata, optsrc, sizeof(struct ptime_data)); } else { memset(newdata, '\0', sizeof(struct ptime_data)); newdata->did_adj4dst = TNYET_ADJ4DST; } return (newdata); } /* * Adjust a given time if that time is in a different timezone than * some other time. */ int ptime_adjust4dst(struct ptime_data *ptime, const struct ptime_data *dstsrc) { struct ptime_data adjtime; if (ptime == NULL) return (-1); /* * Changes are not made to the given time until after all * of the calculations have been successful. */ adjtime = *ptime; /* Check to see if this adjustment was already made */ if ((adjtime.did_adj4dst != TNYET_ADJ4DST) && (adjtime.did_adj4dst == dstsrc->tm.tm_isdst)) return (0); /* yes, so don't make it twice */ /* See if daylight-saving has changed between the two times. */ if (dstsrc->tm.tm_isdst != adjtime.tm.tm_isdst) { if (adjtime.tm.tm_isdst == 1) adjtime.tsecs -= SECS_PER_HOUR; else if (adjtime.tm.tm_isdst == 0) adjtime.tsecs += SECS_PER_HOUR; adjtime.tm = *(localtime(&adjtime.tsecs)); /* Remember that this adjustment has been made */ adjtime.did_adj4dst = dstsrc->tm.tm_isdst; /* * XXX - Should probably check to see if changing the * hour also changed the value of is_dst. What * should we do in that case? */ } *ptime = adjtime; return (0); } int ptime_relparse(struct ptime_data *ptime, int parseopts, time_t basetime, const char *str) { int dpm, pres; struct tm temp_tm; ptime->parseopts = parseopts; ptime->basesecs = basetime; ptime->basetm = *(localtime(&ptime->basesecs)); ptime->tm = ptime->basetm; ptime->tm.tm_hour = ptime->tm.tm_min = ptime->tm.tm_sec = 0; /* * Call a routine which sets ptime.tm and ptime.tspecs based * on the given string and parsing-options. Note that the * routine should not call mktime to set ptime.tsecs. */ if (parseopts & PTM_PARSE_DWM) pres = parseDWM(ptime, str); else pres = parse8601(ptime, str); if (pres < 0) { ptime->tsecs = (time_t)pres; return (pres); } /* * Before calling mktime, check to see if we ended up with a * "day-of-month" that does not exist in the selected month. * If we did call mktime with that info, then mktime will * make it look like the user specifically requested a day * in the following month (eg: Feb 31 turns into Mar 3rd). */ dpm = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); if ((parseopts & PTM_PARSE_MATCHDOM) && (ptime->tmspec & TSPEC_DAYOFMONTH) && (ptime->tm.tm_mday> dpm)) { /* * ptime_nxtime() will want a ptime->tsecs value, * but we need to avoid mktime resetting all the * ptime->tm values. */ if (verbose && dbg_at_times > 1) fprintf(stderr, "\t-- dom fixed: %4d/%02d/%02d %02d:%02d (%02d)", ptime->tm.tm_year, ptime->tm.tm_mon, ptime->tm.tm_mday, ptime->tm.tm_hour, ptime->tm.tm_min, dpm); temp_tm = ptime->tm; ptime->tsecs = mktime(&temp_tm); if (ptime->tsecs > (time_t)-1) ptimeset_nxtime(ptime); if (verbose && dbg_at_times > 1) fprintf(stderr, " to: %4d/%02d/%02d %02d:%02d\n", ptime->tm.tm_year, ptime->tm.tm_mon, ptime->tm.tm_mday, ptime->tm.tm_hour, ptime->tm.tm_min); } /* * Convert the ptime.tm into standard time_t seconds. Check * for invalid times, which includes things like the hour lost * when switching from "standard time" to "daylight saving". */ ptime->tsecs = mktime(&ptime->tm); if (ptime->tsecs == (time_t)-1) { ptime->tsecs = (time_t)-2; return (-2); } return (0); } int ptime_free(struct ptime_data *ptime) { if (ptime == NULL) return (-1); free(ptime); return (0); } /* * Some trivial routines so ptime_data can remain a completely * opaque type. */ const char * ptimeget_ctime(const struct ptime_data *ptime) { if (ptime == NULL) return ("Null time in ptimeget_ctime()\n"); return (ctime(&ptime->tsecs)); } double ptimeget_diff(const struct ptime_data *minuend, const struct ptime_data *subtrahend) { /* Just like difftime(), we have no good error-return */ if (minuend == NULL || subtrahend == NULL) return (0.0); return (difftime(minuend->tsecs, subtrahend->tsecs)); } time_t ptimeget_secs(const struct ptime_data *ptime) { if (ptime == NULL) return (-1); return (ptime->tsecs); } /* * Generate an approximate timestamp for the next event, based on * what parts of time were specified by the original parameter to * ptime_relparse(). The result may be -1 if there is no obvious * "next time" which will work. */ int ptimeset_nxtime(struct ptime_data *ptime) { int moredays, tdpm, tmon, tyear; struct ptime_data nextmatch; if (ptime == NULL) return (-1); /* * Changes are not made to the given time until after all * of the calculations have been successful. */ nextmatch = *ptime; /* * If the user specified a year and we're already past that * time, then there will never be another one! */ if (ptime->tmspec & TSPEC_YEAR) return (-1); /* * The caller gave us a time in the past. Calculate how much * time is needed to go from that valid rotate time to the * next valid rotate time. We only need to get to the nearest * hour, because newsyslog is only run once per hour. */ moredays = 0; if (ptime->tmspec & TSPEC_MONTHOFYEAR) { /* Special case: Feb 29th does not happen every year. */ if (ptime->tm.tm_mon == 1 && ptime->tm.tm_mday == 29) { nextmatch.tm.tm_year += 4; if (days_pmonth(1, nextmatch.tm.tm_year) < 29) nextmatch.tm.tm_year += 4; } else { nextmatch.tm.tm_year += 1; } nextmatch.tm.tm_isdst = -1; nextmatch.tsecs = mktime(&nextmatch.tm); } else if (ptime->tmspec & TSPEC_LDAYOFMONTH) { /* * Need to get to the last day of next month. Origtm is * already at the last day of this month, so just add to * it number of days in the next month. */ if (ptime->tm.tm_mon < 11) moredays = days_pmonth(ptime->tm.tm_mon + 1, ptime->tm.tm_year); else moredays = days_pmonth(0, ptime->tm.tm_year + 1); } else if (ptime->tmspec & TSPEC_DAYOFMONTH) { /* Jump to the same day in the next month */ moredays = days_pmonth(ptime->tm.tm_mon, ptime->tm.tm_year); /* * In some cases, the next month may not *have* the * desired day-of-the-month. If that happens, then * move to the next month that does have enough days. */ tmon = ptime->tm.tm_mon; tyear = ptime->tm.tm_year; for (;;) { if (tmon < 11) tmon += 1; else { tmon = 0; tyear += 1; } tdpm = days_pmonth(tmon, tyear); if (tdpm >= ptime->tm.tm_mday) break; moredays += tdpm; } } else if (ptime->tmspec & TSPEC_DAYOFWEEK) { moredays = 7; } else if (ptime->tmspec & TSPEC_HOUROFDAY) { moredays = 1; } if (moredays != 0) { nextmatch.tsecs += SECS_PER_HOUR * 24 * moredays; nextmatch.tm = *(localtime(&nextmatch.tsecs)); } /* * The new time will need to be adjusted if the setting of * daylight-saving has changed between the two times. */ ptime_adjust4dst(&nextmatch, ptime); /* Everything worked. Update the given time and return. */ *ptime = nextmatch; return (0); } int ptimeset_time(struct ptime_data *ptime, time_t secs) { if (ptime == NULL) return (-1); ptime->tsecs = secs; ptime->tm = *(localtime(&ptime->tsecs)); ptime->parseopts = 0; /* ptime->tmspec = ? */ return (0); }