newsyslog.c revision 111779
167805Scg/*
267805Scg * This file contains changes from the Open Software Foundation.
3319182Sngie */
467805Scg
570711Sobrien/*
670711Sobrien * Copyright 1988, 1989 by the Massachusetts Institute of Technology
770711Sobrien *
870711Sobrien * Permission to use, copy, modify, and distribute this software and its
967805Scg * documentation for any purpose and without fee is hereby granted, provided
10 * that the above copyright notice appear in all copies and that both that
11 * copyright notice and this permission notice appear in supporting
12 * documentation, and that the names of M.I.T. and the M.I.T. S.I.P.B. not be
13 * used in advertising or publicity pertaining to distribution of the
14 * software without specific, written prior permission. M.I.T. and the M.I.T.
15 * S.I.P.B. make no representations about the suitability of this software
16 * for any purpose.  It is provided "as is" without express or implied
17 * warranty.
18 *
19 */
20
21/*
22 * newsyslog - roll over selected logs at the appropriate time, keeping the a
23 * specified number of backup files around.
24 */
25
26#ifndef lint
27static const char rcsid[] =
28"$FreeBSD: head/usr.sbin/newsyslog/newsyslog.c 111779 2003-03-03 01:23:34Z gad $";
29#endif	/* not lint */
30
31#define OSF
32#ifndef COMPRESS_POSTFIX
33#define COMPRESS_POSTFIX ".gz"
34#endif
35#ifndef	BZCOMPRESS_POSTFIX
36#define	BZCOMPRESS_POSTFIX ".bz2"
37#endif
38
39#include <sys/param.h>
40#include <sys/stat.h>
41#include <sys/wait.h>
42
43#include <ctype.h>
44#include <err.h>
45#include <errno.h>
46#include <fcntl.h>
47#include <fnmatch.h>
48#include <glob.h>
49#include <grp.h>
50#include <paths.h>
51#include <pwd.h>
52#include <signal.h>
53#include <stdio.h>
54#include <stdlib.h>
55#include <string.h>
56#include <time.h>
57#include <unistd.h>
58
59#include "pathnames.h"
60
61#define kbytes(size)  (((size) + 1023) >> 10)
62
63#ifdef _IBMR2
64/* Calculates (db * DEV_BSIZE) */
65#define dbtob(db)  ((unsigned)(db) << UBSHIFT)
66#endif
67
68/*
69 * Bit-values for the 'flags' parsed from a config-file entry.
70 */
71#define CE_COMPACT	0x0001	/* Compact the achived log files with gzip. */
72#define CE_BZCOMPACT	0x0002	/* Compact the achived log files with bzip2. */
73#define	CE_COMPACTWAIT	0x0004	/* wait until compressing one file finishes */
74				/*    before starting the next step. */
75#define CE_BINARY	0x0008	/* Logfile is in binary, do not add status */
76				/*    messages to logfile(s) when rotating. */
77#define	CE_NOSIGNAL	0x0010	/* There is no process to signal when */
78				/*    trimming this file. */
79#define	CE_TRIMAT	0x0020	/* trim file at a specific time. */
80#define	CE_GLOB		0x0040	/* name of the log is file name pattern. */
81
82struct conf_entry {
83	char *log;		/* Name of the log */
84	char *pid_file;		/* PID file */
85	char *r_reason;		/* The reason this file is being rotated */
86	int rotate;		/* Non-zero if this file should be rotated */
87	uid_t uid;		/* Owner of log */
88	gid_t gid;		/* Group of log */
89	int numlogs;		/* Number of logs to keep */
90	int size;		/* Size cutoff to trigger trimming the log */
91	int hours;		/* Hours between log trimming */
92	time_t trim_at;		/* Specific time to do trimming */
93	int permissions;	/* File permissions on the log */
94	int flags;		/* CE_COMPACT, CE_BZCOMPACT, CE_BINARY */
95	int sig;		/* Signal to send */
96	int def_cfg;		/* Using the <default> rule for this file */
97	struct conf_entry *next;/* Linked list pointer */
98};
99
100#define DEFAULT_MARKER "<default>"
101
102int archtodir = 0;		/* Archive old logfiles to other directory */
103int verbose = 0;		/* Print out what's going on */
104int needroot = 1;		/* Root privs are necessary */
105int noaction = 0;		/* Don't do anything, just show it */
106int nosignal;			/* Do not send any signals */
107int force = 0;			/* Force the trim no matter what */
108int rotatereq = 0;		/* -R = Always rotate the file(s) as given */
109				/*    on the command (this also requires   */
110				/*    that a list of files *are* given on  */
111				/*    the run command). */
112char *requestor;		/* The name given on a -R request */
113char *archdirname;		/* Directory path to old logfiles archive */
114const char *conf;		/* Configuration file to use */
115time_t timenow;
116
117#define MIN_PID         5
118#define MAX_PID		99999	/* was lower, see /usr/include/sys/proc.h */
119char hostname[MAXHOSTNAMELEN];	/* hostname */
120char daytime[16];		/* timenow in human readable form */
121
122static struct conf_entry *get_worklist(char **files);
123static void parse_file(FILE *cf, const char *cfname, struct conf_entry **work_p,
124		struct conf_entry **defconf_p);
125static char *sob(char *p);
126static char *son(char *p);
127static char *missing_field(char *p, char *errline);
128static void do_entry(struct conf_entry * ent);
129static void free_entry(struct conf_entry *ent);
130static struct conf_entry *init_entry(const char *fname,
131		struct conf_entry *src_entry);
132static void PRS(int argc, char **argv);
133static void usage(void);
134static void dotrim(const struct conf_entry *ent, char *log,
135		int numdays, int flags, int perm, int sig);
136static int log_trim(const char *log, const struct conf_entry *log_ent);
137static void compress_log(char *log, int dowait);
138static void bzcompress_log(char *log, int dowait);
139static int sizefile(char *file);
140static int age_old_log(char *file);
141static pid_t get_pid(const char *pid_file);
142static time_t parse8601(char *s, char *errline);
143static void movefile(char *from, char *to, int perm, uid_t owner_uid,
144		gid_t group_gid);
145static void createdir(char *dirpart);
146static time_t parseDWM(char *s, char *errline);
147
148/*
149 * All the following are defined to work on an 'int', in the
150 * range 0 to 255, plus EOF.  Define wrappers which can take
151 * values of type 'char', either signed or unsigned.
152 */
153#define isprintch(Anychar)    isprint(((int) Anychar) & 255)
154#define isspacech(Anychar)    isspace(((int) Anychar) & 255)
155#define tolowerch(Anychar)    tolower(((int) Anychar) & 255)
156
157int
158main(int argc, char **argv)
159{
160	struct conf_entry *p, *q;
161	char *savglob;
162	glob_t pglob;
163	int i;
164
165	PRS(argc, argv);
166	argc -= optind;
167	argv += optind;
168
169	if (needroot && getuid() && geteuid())
170		errx(1, "must have root privs");
171	p = q = get_worklist(argv);
172
173	while (p) {
174		if ((p->flags & CE_GLOB) == 0) {
175			do_entry(p);
176		} else {
177			if (verbose > 2)
178				printf("\t+ Processing pattern %s\n", p->log);
179			if (glob(p->log, GLOB_NOCHECK, NULL, &pglob) != 0) {
180				warn("can't expand pattern: %s", p->log);
181			} else {
182				savglob = p->log;
183				for (i = 0; i < pglob.gl_matchc; i++) {
184					p->log = pglob.gl_pathv[i];
185					do_entry(p);
186				}
187				globfree(&pglob);
188				p->log = savglob;
189				if (verbose > 2)
190					printf("\t+ Done with pattern\n");
191			}
192		}
193		p = p->next;
194		free_entry(q);
195		q = p;
196	}
197	while (wait(NULL) > 0 || errno == EINTR)
198		;
199	return (0);
200}
201
202static struct conf_entry *
203init_entry(const char *fname, struct conf_entry *src_entry)
204{
205	struct conf_entry *tempwork;
206
207	if (verbose > 4)
208		printf("\t--> [creating entry for %s]\n", fname);
209
210	tempwork = malloc(sizeof(struct conf_entry));
211	if (tempwork == NULL)
212		err(1, "malloc of conf_entry for %s", fname);
213
214	tempwork->log = strdup(fname);
215	if (tempwork->log == NULL)
216		err(1, "strdup for %s", fname);
217
218	if (src_entry != NULL) {
219		tempwork->pid_file = NULL;
220		if (src_entry->pid_file)
221			tempwork->pid_file = strdup(src_entry->pid_file);
222		tempwork->r_reason = NULL;
223		tempwork->rotate = 0;
224		tempwork->uid = src_entry->uid;
225		tempwork->gid = src_entry->gid;
226		tempwork->numlogs = src_entry->numlogs;
227		tempwork->size = src_entry->size;
228		tempwork->hours = src_entry->hours;
229		tempwork->trim_at = src_entry->trim_at;
230		tempwork->permissions = src_entry->permissions;
231		tempwork->flags = src_entry->flags;
232		tempwork->sig = src_entry->sig;
233		tempwork->def_cfg = src_entry->def_cfg;
234	} else {
235		/* Initialize as a "do-nothing" entry */
236		tempwork->pid_file = NULL;
237		tempwork->r_reason = NULL;
238		tempwork->rotate = 0;
239		tempwork->uid = (uid_t)-1;
240		tempwork->gid = (gid_t)-1;
241		tempwork->numlogs = 1;
242		tempwork->size = -1;
243		tempwork->hours = -1;
244		tempwork->trim_at = (time_t)0;
245		tempwork->permissions = 0;
246		tempwork->flags = 0;
247		tempwork->sig = SIGHUP;
248		tempwork->def_cfg = 0;
249	}
250	tempwork->next = NULL;
251
252	return (tempwork);
253}
254
255static void
256free_entry(struct conf_entry *ent)
257{
258
259	if (ent == NULL)
260		return;
261
262	if (ent->log != NULL) {
263		if (verbose > 4)
264			printf("\t--> [freeing entry for %s]\n", ent->log);
265		free(ent->log);
266		ent->log = NULL;
267	}
268
269	if (ent->pid_file != NULL) {
270		free(ent->pid_file);
271		ent->pid_file = NULL;
272	}
273
274	if (ent->r_reason != NULL) {
275		free(ent->r_reason);
276		ent->r_reason = NULL;
277	}
278
279	free(ent);
280}
281
282static void
283do_entry(struct conf_entry * ent)
284{
285#define REASON_MAX	80
286	int size, modtime;
287	char temp_reason[REASON_MAX];
288
289	if (verbose) {
290		if (ent->flags & CE_COMPACT)
291			printf("%s <%dZ>: ", ent->log, ent->numlogs);
292		else if (ent->flags & CE_BZCOMPACT)
293			printf("%s <%dJ>: ", ent->log, ent->numlogs);
294		else
295			printf("%s <%d>: ", ent->log, ent->numlogs);
296	}
297	size = sizefile(ent->log);
298	modtime = age_old_log(ent->log);
299	ent->rotate = 0;
300	if (size < 0) {
301		if (verbose)
302			printf("does not exist.\n");
303	} else {
304		if (ent->flags & CE_TRIMAT && !force && !rotatereq) {
305			if (timenow < ent->trim_at
306			    || difftime(timenow, ent->trim_at) >= 60 * 60) {
307				if (verbose)
308					printf("--> will trim at %s",
309					    ctime(&ent->trim_at));
310				return;
311			} else if (verbose && ent->hours <= 0) {
312				printf("--> time is up\n");
313			}
314		}
315		if (verbose && (ent->size > 0))
316			printf("size (Kb): %d [%d] ", size, ent->size);
317		if (verbose && (ent->hours > 0))
318			printf(" age (hr): %d [%d] ", modtime, ent->hours);
319
320		/*
321		 * Figure out if this logfile needs to be rotated.
322		 */
323		temp_reason[0] = '\0';
324		if (rotatereq) {
325			ent->rotate = 1;
326			snprintf(temp_reason, REASON_MAX, " due to -R from %s",
327			    requestor);
328		} else if (force) {
329			ent->rotate = 1;
330			snprintf(temp_reason, REASON_MAX, " due to -F request");
331		} else if ((ent->size > 0) && (size >= ent->size)) {
332			ent->rotate = 1;
333			snprintf(temp_reason, REASON_MAX, " due to size>%dK",
334			    ent->size);
335		} else if (ent->hours <= 0 && (ent->flags & CE_TRIMAT)) {
336			ent->rotate = 1;
337		} else if ((ent->hours > 0) && ((modtime >= ent->hours) ||
338		    (modtime < 0))) {
339			ent->rotate = 1;
340		}
341
342		/*
343		 * If the file needs to be rotated, then rotate it.
344		 */
345		if (ent->rotate) {
346			if (temp_reason[0] != '\0')
347				ent->r_reason = strdup(temp_reason);
348			if (verbose)
349				printf("--> trimming log....\n");
350			if (noaction && !verbose) {
351				if (ent->flags & CE_COMPACT)
352					printf("%s <%dZ>: trimming\n",
353					    ent->log, ent->numlogs);
354				else if (ent->flags & CE_BZCOMPACT)
355					printf("%s <%dJ>: trimming\n",
356					    ent->log, ent->numlogs);
357				else
358					printf("%s <%d>: trimming\n",
359					    ent->log, ent->numlogs);
360			}
361			dotrim(ent, ent->log, ent->numlogs, ent->flags,
362			    ent->permissions, ent->sig);
363		} else {
364			if (verbose)
365				printf("--> skipping\n");
366		}
367	}
368#undef REASON_MAX
369}
370
371static void
372PRS(int argc, char **argv)
373{
374	int c;
375	char *p;
376
377	timenow = time((time_t *) 0);
378	(void)strncpy(daytime, ctime(&timenow) + 4, 15);
379	daytime[15] = '\0';
380
381	/* Let's get our hostname */
382	(void) gethostname(hostname, sizeof(hostname));
383
384	/* Truncate domain */
385	if ((p = strchr(hostname, '.'))) {
386		*p = '\0';
387	}
388
389	/* Parse command line options. */
390	while ((c = getopt(argc, argv, "a:f:nrsvFR:")) != -1)
391		switch (c) {
392		case 'a':
393			archtodir++;
394			archdirname = optarg;
395			break;
396		case 'f':
397			conf = optarg;
398			break;
399		case 'n':
400			noaction++;
401			break;
402		case 'r':
403			needroot = 0;
404			break;
405		case 's':
406			nosignal = 1;
407			break;
408		case 'v':
409			verbose++;
410			break;
411		case 'F':
412			force++;
413			break;
414		case 'R':
415			rotatereq++;
416			requestor = strdup(optarg);
417			break;
418		default:
419			usage();
420			/* NOTREACHED */
421		}
422
423	if (rotatereq) {
424		if (optind == argc) {
425			warnx("At least one filename must be given when -R is specified.");
426			usage();
427			/* NOTREACHED */
428		}
429		/* Make sure "requestor" value is safe for a syslog message. */
430		for (p = requestor; *p != '\0'; p++) {
431			if (!isprintch(*p) && (*p != '\t'))
432				*p = '.';
433		}
434	}
435}
436
437static void
438usage(void)
439{
440
441	fprintf(stderr,
442	    "usage: newsyslog [-Fnrsv] [-a directory] [-f config-file]\n"
443	    "                 [ [-R requestor] filename ... ]\n");
444	exit(1);
445}
446
447/*
448 * Parse a configuration file and return a linked list of all the logs
449 * which should be processed.
450 */
451static struct conf_entry *
452get_worklist(char **files)
453{
454	FILE *f;
455	const char *fname;
456	char **given;
457	struct conf_entry *defconf, *dupent, *ent, *firstnew;
458	struct conf_entry *newlist, *worklist;
459	int gmatch;
460
461	defconf = worklist = NULL;
462
463	fname = conf;
464	if (fname == NULL)
465		fname = _PATH_CONF;
466
467	if (strcmp(fname, "-") != 0)
468		f = fopen(fname, "r");
469	else {
470		f = stdin;
471		fname = "<stdin>";
472	}
473	if (!f)
474		err(1, "%s", conf);
475
476	parse_file(f, fname, &worklist, &defconf);
477	(void) fclose(f);
478
479	/*
480	 * All config-file information has been read in and turned into
481	 * a worklist.  If there were no specific files given on the run
482	 * command, then the work of this routine is done.
483	 */
484	if (*files == NULL) {
485		if (defconf != NULL)
486			free_entry(defconf);
487		return (worklist);
488		/* NOTREACHED */
489	}
490
491	/*
492	 * If newsyslog was given a specific list of files to process,
493	 * it may be that some of those files were not listed in any
494	 * config file.  Those unlisted files should get the default
495	 * rotation action.  First, create the default-rotation action
496	 * if none was found in a system config file.
497	 */
498	if (defconf == NULL) {
499		defconf = init_entry(DEFAULT_MARKER, NULL);
500		defconf->numlogs = 3;
501		defconf->size = 50;
502		defconf->permissions = S_IRUSR|S_IWUSR;
503	}
504
505	/*
506	 * If newsyslog was run with a list of specific filenames,
507	 * then create a new worklist which has only those files in
508	 * it, picking up the rotation-rules for those files from
509	 * the original worklist.
510	 *
511	 * XXX - Note that this will copy multiple rules for a single
512	 *	logfile, if multiple entries are an exact match for
513	 *	that file.  That matches the historic behavior, but do
514	 *	we want to continue to allow it?  If so, it should
515	 *	probably be handled more intelligently.
516	 */
517	firstnew = newlist = NULL;
518	for (given = files; *given; ++given) {
519		gmatch = 0;
520		/*
521		 * First try to find exact-matches for this given file.
522		 */
523		for (ent = worklist; ent; ent = ent->next) {
524			if ((ent->flags & CE_GLOB) != 0)
525				continue;
526			if (strcmp(ent->log, *given) == 0) {
527				gmatch++;
528				dupent = init_entry(*given, ent);
529				if (!firstnew)
530					firstnew = dupent;
531				else
532					newlist->next = dupent;
533				newlist = dupent;
534			}
535		}
536		if (gmatch) {
537			if (verbose > 2)
538				printf("\t+ Matched entry %s\n", *given);
539			continue;
540		}
541
542		/*
543		 * There was no exact-match for this given file, so look
544		 * for a "glob" entry which does match.
545		 */
546		for (ent = worklist; ent; ent = ent->next) {
547			if ((ent->flags & CE_GLOB) == 0)
548				continue;
549			if (fnmatch(ent->log, *given, FNM_PATHNAME) == 0) {
550				gmatch++;
551				dupent = init_entry(*given, ent);
552				if (!firstnew)
553					firstnew = dupent;
554				else
555					newlist->next = dupent;
556				newlist = dupent;
557				/* This work entry is *not* a glob! */
558				dupent->flags &= ~CE_GLOB;
559				/* Only allow a match to one glob-entry */
560				break;
561			}
562		}
563		if (gmatch) {
564			if (verbose > 2)
565				printf("\t+ Matched %s via %s\n", *given,
566				    ent->log);
567			continue;
568		}
569
570		/*
571		 * This given file was not found in any config file, so
572		 * add a worklist item based on the default entry.
573		 */
574		if (verbose > 2)
575			printf("\t+ No entry matched %s  (will use %s)\n",
576			    *given, DEFAULT_MARKER);
577		dupent = init_entry(*given, defconf);
578		if (!firstnew)
579			firstnew = dupent;
580		else
581			newlist->next = dupent;
582		/* Mark that it was *not* found in a config file */
583		dupent->def_cfg = 1;
584		newlist = dupent;
585	}
586
587	/*
588	 * Free all the entries in the original work list, and then
589	 * return the new work list.
590	 */
591	while (worklist) {
592		ent = worklist->next;
593		free_entry(worklist);
594		worklist = ent;
595	}
596
597	free_entry(defconf);
598	return (newlist);
599}
600
601/*
602 * Parse a configuration file and update a linked list of all the logs to
603 * process.
604 */
605static void
606parse_file(FILE *cf, const char *cfname, struct conf_entry **work_p,
607    struct conf_entry **defconf_p)
608{
609	char line[BUFSIZ], *parse, *q;
610	char *cp, *errline, *group;
611	struct conf_entry *working, *worklist;
612	struct passwd *pass;
613	struct group *grp;
614	int eol;
615
616	/*
617	 * XXX - for now, assume that only one config file will be read,
618	 *	ie, this routine is only called one time.
619	 */
620	worklist = NULL;
621
622	while (fgets(line, BUFSIZ, cf)) {
623		if ((line[0] == '\n') || (line[0] == '#') ||
624		    (strlen(line) == 0))
625			continue;
626		errline = strdup(line);
627		for (cp = line + 1; *cp != '\0'; cp++) {
628			if (*cp != '#')
629				continue;
630			if (*(cp - 1) == '\\') {
631				strcpy(cp - 1, cp);
632				cp--;
633				continue;
634			}
635			*cp = '\0';
636			break;
637		}
638
639		q = parse = missing_field(sob(line), errline);
640		parse = son(line);
641		if (!*parse)
642			errx(1, "malformed line (missing fields):\n%s",
643			    errline);
644		*parse = '\0';
645
646		working = init_entry(q, NULL);
647		if (strcasecmp(DEFAULT_MARKER, q) == 0) {
648			if (defconf_p == NULL) {
649				warnx("Ignoring entry for %s in %s!", q,
650				    cfname);
651				free_entry(working);
652				continue;
653			} else if (*defconf_p != NULL) {
654				warnx("Ignoring duplicate entry for %s!", q);
655				free_entry(working);
656				continue;
657			}
658			*defconf_p = working;
659		} else {
660			if (!*work_p)
661				*work_p = working;
662			else
663				worklist->next = working;
664			worklist = working;
665		}
666
667		q = parse = missing_field(sob(++parse), errline);
668		parse = son(parse);
669		if (!*parse)
670			errx(1, "malformed line (missing fields):\n%s",
671			    errline);
672		*parse = '\0';
673		if ((group = strchr(q, ':')) != NULL ||
674		    (group = strrchr(q, '.')) != NULL) {
675			*group++ = '\0';
676			if (*q) {
677				if (!(isnumber(*q))) {
678					if ((pass = getpwnam(q)) == NULL)
679						errx(1,
680				     "error in config file; unknown user:\n%s",
681						    errline);
682					working->uid = pass->pw_uid;
683				} else
684					working->uid = atoi(q);
685			} else
686				working->uid = (uid_t)-1;
687
688			q = group;
689			if (*q) {
690				if (!(isnumber(*q))) {
691					if ((grp = getgrnam(q)) == NULL)
692						errx(1,
693				    "error in config file; unknown group:\n%s",
694						    errline);
695					working->gid = grp->gr_gid;
696				} else
697					working->gid = atoi(q);
698			} else
699				working->gid = (gid_t)-1;
700
701			q = parse = missing_field(sob(++parse), errline);
702			parse = son(parse);
703			if (!*parse)
704				errx(1, "malformed line (missing fields):\n%s",
705				    errline);
706			*parse = '\0';
707		} else {
708			working->uid = (uid_t)-1;
709			working->gid = (gid_t)-1;
710		}
711
712		if (!sscanf(q, "%o", &working->permissions))
713			errx(1, "error in config file; bad permissions:\n%s",
714			    errline);
715
716		q = parse = missing_field(sob(++parse), errline);
717		parse = son(parse);
718		if (!*parse)
719			errx(1, "malformed line (missing fields):\n%s",
720			    errline);
721		*parse = '\0';
722		if (!sscanf(q, "%d", &working->numlogs) || working->numlogs < 0)
723			errx(1, "error in config file; bad value for count of logs to save:\n%s",
724			    errline);
725
726		q = parse = missing_field(sob(++parse), errline);
727		parse = son(parse);
728		if (!*parse)
729			errx(1, "malformed line (missing fields):\n%s",
730			    errline);
731		*parse = '\0';
732		if (isdigit(*q))
733			working->size = atoi(q);
734		else
735			working->size = -1;
736
737		working->flags = 0;
738		q = parse = missing_field(sob(++parse), errline);
739		parse = son(parse);
740		eol = !*parse;
741		*parse = '\0';
742		{
743			char *ep;
744			u_long ul;
745
746			ul = strtoul(q, &ep, 10);
747			if (ep == q)
748				working->hours = 0;
749			else if (*ep == '*')
750				working->hours = -1;
751			else if (ul > INT_MAX)
752				errx(1, "interval is too large:\n%s", errline);
753			else
754				working->hours = ul;
755
756			if (*ep != '\0' && *ep != '@' && *ep != '*' &&
757			    *ep != '$')
758				errx(1, "malformed interval/at:\n%s", errline);
759			if (*ep == '@') {
760				if ((working->trim_at = parse8601(ep + 1, errline))
761				    == (time_t) - 1)
762					errx(1, "malformed at:\n%s", errline);
763				working->flags |= CE_TRIMAT;
764			} else if (*ep == '$') {
765				if ((working->trim_at = parseDWM(ep + 1, errline))
766				    == (time_t) - 1)
767					errx(1, "malformed at:\n%s", errline);
768				working->flags |= CE_TRIMAT;
769			}
770		}
771
772		if (eol)
773			q = NULL;
774		else {
775			q = parse = sob(++parse);	/* Optional field */
776			parse = son(parse);
777			if (!*parse)
778				eol = 1;
779			*parse = '\0';
780		}
781
782		for (; q && *q && !isspacech(*q); q++) {
783			switch (tolowerch(*q)) {
784			case 'b':
785				working->flags |= CE_BINARY;
786				break;
787			case 'c':
788				/*
789				 * netbsd uses 'c' for "create".  We will
790				 * temporarily accept it for 'g', because
791				 * earlier freebsd versions had a typo
792				 * of ('G' || 'c')...
793				 */
794				warnx("Assuming 'g' for 'c' in flags for line:\n%s",
795				    errline);
796				/* FALLTHROUGH */
797			case 'g':
798				working->flags |= CE_GLOB;
799				break;
800			case 'j':
801				working->flags |= CE_BZCOMPACT;
802				break;
803			case 'n':
804				working->flags |= CE_NOSIGNAL;
805				break;
806			case 'w':
807				working->flags |= CE_COMPACTWAIT;
808				break;
809			case 'z':
810				working->flags |= CE_COMPACT;
811				break;
812			case '-':
813				break;
814			default:
815				errx(1, "illegal flag in config file -- %c",
816				    *q);
817			}
818		}
819
820		if (eol)
821			q = NULL;
822		else {
823			q = parse = sob(++parse);	/* Optional field */
824			parse = son(parse);
825			if (!*parse)
826				eol = 1;
827			*parse = '\0';
828		}
829
830		working->pid_file = NULL;
831		if (q && *q) {
832			if (*q == '/')
833				working->pid_file = strdup(q);
834			else if (isdigit(*q))
835				goto got_sig;
836			else
837				errx(1,
838			"illegal pid file or signal number in config file:\n%s",
839				    errline);
840		}
841		if (eol)
842			q = NULL;
843		else {
844			q = parse = sob(++parse);	/* Optional field */
845			*(parse = son(parse)) = '\0';
846		}
847
848		working->sig = SIGHUP;
849		if (q && *q) {
850			if (isdigit(*q)) {
851		got_sig:
852				working->sig = atoi(q);
853			} else {
854		err_sig:
855				errx(1,
856				    "illegal signal number in config file:\n%s",
857				    errline);
858			}
859			if (working->sig < 1 || working->sig >= NSIG)
860				goto err_sig;
861		}
862
863		/*
864		 * Finish figuring out what pid-file to use (if any) in
865		 * later processing if this logfile needs to be rotated.
866		 */
867		if ((working->flags & CE_NOSIGNAL) == CE_NOSIGNAL) {
868			/*
869			 * This config-entry specified 'n' for nosignal,
870			 * see if it also specified an explicit pid_file.
871			 * This would be a pretty pointless combination.
872			 */
873			if (working->pid_file != NULL) {
874				warnx("Ignoring '%s' because flag 'n' was specified in line:\n%s",
875				    working->pid_file, errline);
876				free(working->pid_file);
877				working->pid_file = NULL;
878			}
879		} else if (nosignal) {
880			/*
881			 * While this entry might usually signal some
882			 * process via the pid-file, newsyslog was run
883			 * with '-s', so quietly ignore the pid-file.
884			 */
885			if (working->pid_file != NULL) {
886				free(working->pid_file);
887				working->pid_file = NULL;
888			}
889		} else if (working->pid_file == NULL) {
890			/*
891			 * This entry did not specify the 'n' flag, which
892			 * means it should signal syslogd unless it had
893			 * specified some other pid-file.  But we only
894			 * try to notify syslog if we are root
895			 */
896			if (needroot)
897				working->pid_file = strdup(_PATH_SYSLOGPID);
898		}
899
900		free(errline);
901		errline = NULL;
902	}
903}
904
905static char *
906missing_field(char *p, char *errline)
907{
908
909	if (!p || !*p)
910		errx(1, "missing field in config file:\n%s", errline);
911	return (p);
912}
913
914static void
915dotrim(const struct conf_entry *ent, char *log, int numdays, int flags,
916    int perm, int sig)
917{
918	char dirpart[MAXPATHLEN], namepart[MAXPATHLEN];
919	char file1[MAXPATHLEN], file2[MAXPATHLEN];
920	char zfile1[MAXPATHLEN], zfile2[MAXPATHLEN];
921	char jfile1[MAXPATHLEN];
922	char tfile[MAXPATHLEN];
923	int notified, need_notification, fd, _numdays;
924	struct stat st;
925	pid_t pid;
926
927	if (archtodir) {
928		char *p;
929
930		/* build complete name of archive directory into dirpart */
931		if (*archdirname == '/') {	/* absolute */
932			strlcpy(dirpart, archdirname, sizeof(dirpart));
933		} else {	/* relative */
934			/* get directory part of logfile */
935			strlcpy(dirpart, log, sizeof(dirpart));
936			if ((p = rindex(dirpart, '/')) == NULL)
937				dirpart[0] = '\0';
938			else
939				*(p + 1) = '\0';
940			strlcat(dirpart, archdirname, sizeof(dirpart));
941		}
942
943		/* check if archive directory exists, if not, create it */
944		if (lstat(dirpart, &st))
945			createdir(dirpart);
946
947		/* get filename part of logfile */
948		if ((p = rindex(log, '/')) == NULL)
949			strlcpy(namepart, log, sizeof(namepart));
950		else
951			strlcpy(namepart, p + 1, sizeof(namepart));
952
953		/* name of oldest log */
954		(void) snprintf(file1, sizeof(file1), "%s/%s.%d", dirpart,
955		    namepart, numdays);
956		(void) snprintf(zfile1, sizeof(zfile1), "%s%s", file1,
957		    COMPRESS_POSTFIX);
958		snprintf(jfile1, sizeof(jfile1), "%s%s", file1,
959		    BZCOMPRESS_POSTFIX);
960	} else {
961		/* name of oldest log */
962		(void) snprintf(file1, sizeof(file1), "%s.%d", log, numdays);
963		(void) snprintf(zfile1, sizeof(zfile1), "%s%s", file1,
964		    COMPRESS_POSTFIX);
965		snprintf(jfile1, sizeof(jfile1), "%s%s", file1,
966		    BZCOMPRESS_POSTFIX);
967	}
968
969	if (noaction) {
970		printf("rm -f %s\n", file1);
971		printf("rm -f %s\n", zfile1);
972		printf("rm -f %s\n", jfile1);
973	} else {
974		(void) unlink(file1);
975		(void) unlink(zfile1);
976		(void) unlink(jfile1);
977	}
978
979	/* Move down log files */
980	_numdays = numdays;	/* preserve */
981	while (numdays--) {
982
983		(void) strlcpy(file2, file1, sizeof(file2));
984
985		if (archtodir)
986			(void) snprintf(file1, sizeof(file1), "%s/%s.%d",
987			    dirpart, namepart, numdays);
988		else
989			(void) snprintf(file1, sizeof(file1), "%s.%d", log,
990			    numdays);
991
992		(void) strlcpy(zfile1, file1, sizeof(zfile1));
993		(void) strlcpy(zfile2, file2, sizeof(zfile2));
994		if (lstat(file1, &st)) {
995			(void) strlcat(zfile1, COMPRESS_POSTFIX,
996			    sizeof(zfile1));
997			(void) strlcat(zfile2, COMPRESS_POSTFIX,
998			    sizeof(zfile2));
999			if (lstat(zfile1, &st)) {
1000				strlcpy(zfile1, file1, sizeof(zfile1));
1001				strlcpy(zfile2, file2, sizeof(zfile2));
1002				strlcat(zfile1, BZCOMPRESS_POSTFIX,
1003				    sizeof(zfile1));
1004				strlcat(zfile2, BZCOMPRESS_POSTFIX,
1005				    sizeof(zfile2));
1006				if (lstat(zfile1, &st))
1007					continue;
1008			}
1009		}
1010		if (noaction) {
1011			printf("mv %s %s\n", zfile1, zfile2);
1012			printf("chmod %o %s\n", perm, zfile2);
1013			if (ent->uid != (uid_t)-1 || ent->gid != (gid_t)-1)
1014				printf("chown %u:%u %s\n",
1015				    ent->uid, ent->gid, zfile2);
1016		} else {
1017			(void) rename(zfile1, zfile2);
1018			(void) chmod(zfile2, perm);
1019			if (ent->uid != (uid_t)-1 || ent->gid != (gid_t)-1)
1020				if (chown(zfile2, ent->uid, ent->gid))
1021					warn("can't chown %s", zfile2);
1022		}
1023	}
1024	if (!noaction && !(flags & CE_BINARY)) {
1025		/* Report the trimming to the old log */
1026		(void) log_trim(log, ent);
1027	}
1028
1029	if (!_numdays) {
1030		if (noaction)
1031			printf("rm %s\n", log);
1032		else
1033			(void) unlink(log);
1034	} else {
1035		if (noaction)
1036			printf("mv %s to %s\n", log, file1);
1037		else {
1038			if (archtodir)
1039				movefile(log, file1, perm, ent->uid,
1040				    ent->gid);
1041			else
1042				(void) rename(log, file1);
1043		}
1044	}
1045
1046	if (noaction)
1047		printf("Start new log...");
1048	else {
1049		strlcpy(tfile, log, sizeof(tfile));
1050		strlcat(tfile, ".XXXXXX", sizeof(tfile));
1051		mkstemp(tfile);
1052		fd = creat(tfile, perm);
1053		if (fd < 0)
1054			err(1, "can't start new log");
1055		if (ent->uid != (uid_t)-1 || ent->gid != (gid_t)-1)
1056			if (fchown(fd, ent->uid, ent->gid))
1057			    err(1, "can't chown new log file");
1058		(void) close(fd);
1059		if (!(flags & CE_BINARY)) {
1060			/* Add status message to new log file */
1061			if (log_trim(tfile, ent))
1062				err(1, "can't add status message to log");
1063		}
1064	}
1065	if (noaction)
1066		printf("chmod %o %s...\n", perm, log);
1067	else {
1068		(void) chmod(tfile, perm);
1069		if (rename(tfile, log) < 0) {
1070			err(1, "can't start new log");
1071			(void) unlink(tfile);
1072		}
1073	}
1074
1075	pid = 0;
1076	need_notification = notified = 0;
1077	if (ent->pid_file != NULL) {
1078		need_notification = 1;
1079		pid = get_pid(ent->pid_file);
1080	}
1081	if (pid) {
1082		if (noaction) {
1083			notified = 1;
1084			printf("kill -%d %d\n", sig, (int) pid);
1085		} else if (kill(pid, sig))
1086			warn("can't notify daemon, pid %d", (int) pid);
1087		else {
1088			notified = 1;
1089			if (verbose)
1090				printf("daemon pid %d notified\n", (int) pid);
1091		}
1092	}
1093	if ((flags & CE_COMPACT) || (flags & CE_BZCOMPACT)) {
1094		if (need_notification && !notified)
1095			warnx(
1096			    "log %s not compressed because daemon not notified",
1097			    log);
1098		else if (noaction)
1099			printf("Compress %s.0\n", log);
1100		else {
1101			if (notified) {
1102				if (verbose)
1103					printf("small pause to allow daemon to close log\n");
1104				sleep(10);
1105			}
1106			if (archtodir) {
1107				(void) snprintf(file1, sizeof(file1), "%s/%s",
1108				    dirpart, namepart);
1109				if (flags & CE_COMPACT)
1110					compress_log(file1,
1111					    flags & CE_COMPACTWAIT);
1112				else if (flags & CE_BZCOMPACT)
1113					bzcompress_log(file1,
1114					    flags & CE_COMPACTWAIT);
1115			} else {
1116				if (flags & CE_COMPACT)
1117					compress_log(log,
1118					    flags & CE_COMPACTWAIT);
1119				else if (flags & CE_BZCOMPACT)
1120					bzcompress_log(log,
1121					    flags & CE_COMPACTWAIT);
1122			}
1123		}
1124	}
1125}
1126
1127/* Log the fact that the logs were turned over */
1128static int
1129log_trim(const char *log, const struct conf_entry *log_ent)
1130{
1131	FILE *f;
1132	const char *xtra;
1133
1134	if ((f = fopen(log, "a")) == NULL)
1135		return (-1);
1136	xtra = "";
1137	if (log_ent->def_cfg)
1138		xtra = " using <default> rule";
1139	if (log_ent->r_reason != NULL)
1140		fprintf(f, "%s %s newsyslog[%d]: logfile turned over%s%s\n",
1141		    daytime, hostname, (int) getpid(), log_ent->r_reason, xtra);
1142	else
1143		fprintf(f, "%s %s newsyslog[%d]: logfile turned over%s\n",
1144		    daytime, hostname, (int) getpid(), xtra);
1145	if (fclose(f) == EOF)
1146		err(1, "log_trim: fclose:");
1147	return (0);
1148}
1149
1150/* Fork of gzip to compress the old log file */
1151static void
1152compress_log(char *log, int dowait)
1153{
1154	pid_t pid;
1155	char tmp[MAXPATHLEN];
1156
1157	while (dowait && (wait(NULL) > 0 || errno == EINTR))
1158		;
1159	(void) snprintf(tmp, sizeof(tmp), "%s.0", log);
1160	pid = fork();
1161	if (pid < 0)
1162		err(1, "gzip fork");
1163	else if (!pid) {
1164		(void) execl(_PATH_GZIP, _PATH_GZIP, "-f", tmp, (char *)0);
1165		err(1, _PATH_GZIP);
1166	}
1167}
1168
1169/* Fork of bzip2 to compress the old log file */
1170static void
1171bzcompress_log(char *log, int dowait)
1172{
1173	pid_t pid;
1174	char tmp[MAXPATHLEN];
1175
1176	while (dowait && (wait(NULL) > 0 || errno == EINTR))
1177		;
1178	snprintf(tmp, sizeof(tmp), "%s.0", log);
1179	pid = fork();
1180	if (pid < 0)
1181		err(1, "bzip2 fork");
1182	else if (!pid) {
1183		execl(_PATH_BZIP2, _PATH_BZIP2, "-f", tmp, (char *)0);
1184		err(1, _PATH_BZIP2);
1185	}
1186}
1187
1188/* Return size in kilobytes of a file */
1189static int
1190sizefile(char *file)
1191{
1192	struct stat sb;
1193
1194	if (stat(file, &sb) < 0)
1195		return (-1);
1196	return (kbytes(dbtob(sb.st_blocks)));
1197}
1198
1199/* Return the age of old log file (file.0) */
1200static int
1201age_old_log(char *file)
1202{
1203	struct stat sb;
1204	char tmp[MAXPATHLEN + sizeof(".0") + sizeof(COMPRESS_POSTFIX) + 1];
1205
1206	if (archtodir) {
1207		char *p;
1208
1209		/* build name of archive directory into tmp */
1210		if (*archdirname == '/') {	/* absolute */
1211			strlcpy(tmp, archdirname, sizeof(tmp));
1212		} else {	/* relative */
1213			/* get directory part of logfile */
1214			strlcpy(tmp, file, sizeof(tmp));
1215			if ((p = rindex(tmp, '/')) == NULL)
1216				tmp[0] = '\0';
1217			else
1218				*(p + 1) = '\0';
1219			strlcat(tmp, archdirname, sizeof(tmp));
1220		}
1221
1222		strlcat(tmp, "/", sizeof(tmp));
1223
1224		/* get filename part of logfile */
1225		if ((p = rindex(file, '/')) == NULL)
1226			strlcat(tmp, file, sizeof(tmp));
1227		else
1228			strlcat(tmp, p + 1, sizeof(tmp));
1229	} else {
1230		(void) strlcpy(tmp, file, sizeof(tmp));
1231	}
1232
1233	if (stat(strcat(tmp, ".0"), &sb) < 0)
1234		if (stat(strcat(tmp, COMPRESS_POSTFIX), &sb) < 0)
1235			return (-1);
1236	return ((int) (timenow - sb.st_mtime + 1800) / 3600);
1237}
1238
1239static pid_t
1240get_pid(const char *pid_file)
1241{
1242	FILE *f;
1243	char line[BUFSIZ];
1244	pid_t pid = 0;
1245
1246	if ((f = fopen(pid_file, "r")) == NULL)
1247		warn("can't open %s pid file to restart a daemon",
1248		    pid_file);
1249	else {
1250		if (fgets(line, BUFSIZ, f)) {
1251			pid = atol(line);
1252			if (pid < MIN_PID || pid > MAX_PID) {
1253				warnx("preposterous process number: %d",
1254				   (int)pid);
1255				pid = 0;
1256			}
1257		} else
1258			warn("can't read %s pid file to restart a daemon",
1259			    pid_file);
1260		(void) fclose(f);
1261	}
1262	return (pid);
1263}
1264
1265/* Skip Over Blanks */
1266char *
1267sob(char *p)
1268{
1269	while (p && *p && isspace(*p))
1270		p++;
1271	return (p);
1272}
1273
1274/* Skip Over Non-Blanks */
1275char *
1276son(char *p)
1277{
1278	while (p && *p && !isspace(*p))
1279		p++;
1280	return (p);
1281}
1282
1283/*
1284 * Parse a limited subset of ISO 8601. The specific format is as follows:
1285 *
1286 * [CC[YY[MM[DD]]]][THH[MM[SS]]]	(where `T' is the literal letter)
1287 *
1288 * We don't accept a timezone specification; missing fields (including timezone)
1289 * are defaulted to the current date but time zero.
1290 */
1291static time_t
1292parse8601(char *s, char *errline)
1293{
1294	char *t;
1295	time_t tsecs;
1296	struct tm tm, *tmp;
1297	u_long ul;
1298
1299	tmp = localtime(&timenow);
1300	tm = *tmp;
1301
1302	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1303
1304	ul = strtoul(s, &t, 10);
1305	if (*t != '\0' && *t != 'T')
1306		return (-1);
1307
1308	/*
1309	 * Now t points either to the end of the string (if no time was
1310	 * provided) or to the letter `T' which separates date and time in
1311	 * ISO 8601.  The pointer arithmetic is the same for either case.
1312	 */
1313	switch (t - s) {
1314	case 8:
1315		tm.tm_year = ((ul / 1000000) - 19) * 100;
1316		ul = ul % 1000000;
1317	case 6:
1318		tm.tm_year -= tm.tm_year % 100;
1319		tm.tm_year += ul / 10000;
1320		ul = ul % 10000;
1321	case 4:
1322		tm.tm_mon = (ul / 100) - 1;
1323		ul = ul % 100;
1324	case 2:
1325		tm.tm_mday = ul;
1326	case 0:
1327		break;
1328	default:
1329		return (-1);
1330	}
1331
1332	/* sanity check */
1333	if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12
1334	    || tm.tm_mday < 1 || tm.tm_mday > 31)
1335		return (-1);
1336
1337	if (*t != '\0') {
1338		s = ++t;
1339		ul = strtoul(s, &t, 10);
1340		if (*t != '\0' && !isspace(*t))
1341			return (-1);
1342
1343		switch (t - s) {
1344		case 6:
1345			tm.tm_sec = ul % 100;
1346			ul /= 100;
1347		case 4:
1348			tm.tm_min = ul % 100;
1349			ul /= 100;
1350		case 2:
1351			tm.tm_hour = ul;
1352		case 0:
1353			break;
1354		default:
1355			return (-1);
1356		}
1357
1358		/* sanity check */
1359		if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0
1360		    || tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23)
1361			return (-1);
1362	}
1363	if ((tsecs = mktime(&tm)) == -1)
1364		errx(1, "nonexistent time:\n%s", errline);
1365	return (tsecs);
1366}
1367
1368/* physically move file */
1369static void
1370movefile(char *from, char *to, int perm, uid_t owner_uid, gid_t group_gid)
1371{
1372	FILE *src, *dst;
1373	int c;
1374
1375	if ((src = fopen(from, "r")) == NULL)
1376		err(1, "can't fopen %s for reading", from);
1377	if ((dst = fopen(to, "w")) == NULL)
1378		err(1, "can't fopen %s for writing", to);
1379	if (owner_uid != (uid_t)-1 || group_gid != (gid_t)-1) {
1380		if (fchown(fileno(dst), owner_uid, group_gid))
1381			err(1, "can't fchown %s", to);
1382	}
1383	if (fchmod(fileno(dst), perm))
1384		err(1, "can't fchmod %s", to);
1385
1386	while ((c = getc(src)) != EOF) {
1387		if ((putc(c, dst)) == EOF)
1388			err(1, "error writing to %s", to);
1389	}
1390
1391	if (ferror(src))
1392		err(1, "error reading from %s", from);
1393	if ((fclose(src)) != 0)
1394		err(1, "can't fclose %s", to);
1395	if ((fclose(dst)) != 0)
1396		err(1, "can't fclose %s", from);
1397	if ((unlink(from)) != 0)
1398		err(1, "can't unlink %s", from);
1399}
1400
1401/* create one or more directory components of a path */
1402static void
1403createdir(char *dirpart)
1404{
1405	int res;
1406	char *s, *d;
1407	char mkdirpath[MAXPATHLEN];
1408	struct stat st;
1409
1410	s = dirpart;
1411	d = mkdirpath;
1412
1413	for (;;) {
1414		*d++ = *s++;
1415		if (*s != '/' && *s != '\0')
1416			continue;
1417		*d = '\0';
1418		res = lstat(mkdirpath, &st);
1419		if (res != 0) {
1420			if (noaction) {
1421				printf("mkdir %s\n", mkdirpath);
1422			} else {
1423				res = mkdir(mkdirpath, 0755);
1424				if (res != 0)
1425					err(1, "Error on mkdir(\"%s\") for -a",
1426					    mkdirpath);
1427			}
1428		}
1429		if (*s == '\0')
1430			break;
1431	}
1432	if (verbose)
1433		printf("created directory '%s' for -a\n", dirpart);
1434}
1435
1436/*-
1437 * Parse a cyclic time specification, the format is as follows:
1438 *
1439 *	[Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
1440 *
1441 * to rotate a logfile cyclic at
1442 *
1443 *	- every day (D) within a specific hour (hh)	(hh = 0...23)
1444 *	- once a week (W) at a specific day (d)     OR	(d = 0..6, 0 = Sunday)
1445 *	- once a month (M) at a specific day (d)	(d = 1..31,l|L)
1446 *
1447 * We don't accept a timezone specification; missing fields
1448 * are defaulted to the current date but time zero.
1449 */
1450static time_t
1451parseDWM(char *s, char *errline)
1452{
1453	char *t;
1454	time_t tsecs;
1455	struct tm tm, *tmp;
1456	long l;
1457	int nd;
1458	static int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1459	int WMseen = 0;
1460	int Dseen = 0;
1461
1462	tmp = localtime(&timenow);
1463	tm = *tmp;
1464
1465	/* set no. of days per month */
1466
1467	nd = mtab[tm.tm_mon];
1468
1469	if (tm.tm_mon == 1) {
1470		if (((tm.tm_year + 1900) % 4 == 0) &&
1471		    ((tm.tm_year + 1900) % 100 != 0) &&
1472		    ((tm.tm_year + 1900) % 400 == 0)) {
1473			nd++;	/* leap year, 29 days in february */
1474		}
1475	}
1476	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1477
1478	for (;;) {
1479		switch (*s) {
1480		case 'D':
1481			if (Dseen)
1482				return (-1);
1483			Dseen++;
1484			s++;
1485			l = strtol(s, &t, 10);
1486			if (l < 0 || l > 23)
1487				return (-1);
1488			tm.tm_hour = l;
1489			break;
1490
1491		case 'W':
1492			if (WMseen)
1493				return (-1);
1494			WMseen++;
1495			s++;
1496			l = strtol(s, &t, 10);
1497			if (l < 0 || l > 6)
1498				return (-1);
1499			if (l != tm.tm_wday) {
1500				int save;
1501
1502				if (l < tm.tm_wday) {
1503					save = 6 - tm.tm_wday;
1504					save += (l + 1);
1505				} else {
1506					save = l - tm.tm_wday;
1507				}
1508
1509				tm.tm_mday += save;
1510
1511				if (tm.tm_mday > nd) {
1512					tm.tm_mon++;
1513					tm.tm_mday = tm.tm_mday - nd;
1514				}
1515			}
1516			break;
1517
1518		case 'M':
1519			if (WMseen)
1520				return (-1);
1521			WMseen++;
1522			s++;
1523			if (tolower(*s) == 'l') {
1524				tm.tm_mday = nd;
1525				s++;
1526				t = s;
1527			} else {
1528				l = strtol(s, &t, 10);
1529				if (l < 1 || l > 31)
1530					return (-1);
1531
1532				if (l > nd)
1533					return (-1);
1534				tm.tm_mday = l;
1535			}
1536			break;
1537
1538		default:
1539			return (-1);
1540			break;
1541		}
1542
1543		if (*t == '\0' || isspace(*t))
1544			break;
1545		else
1546			s = t;
1547	}
1548	if ((tsecs = mktime(&tm)) == -1)
1549		errx(1, "nonexistent time:\n%s", errline);
1550	return (tsecs);
1551}
1552