newsyslog.c revision 71299
1/*
2 * This file contains changes from the Open Software Foundation.
3 */
4
5/*
6 * Copyright 1988, 1989 by the Massachusetts Institute of Technology
7 *
8 * Permission to use, copy, modify, and distribute this software and its
9 * 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 71299 2001-01-20 17:36:08Z jedgar $";
29#endif	/* not lint */
30
31#define OSF
32#ifndef COMPRESS_POSTFIX
33#define COMPRESS_POSTFIX ".gz"
34#endif
35
36#include <ctype.h>
37#include <err.h>
38#include <fcntl.h>
39#include <grp.h>
40#include <paths.h>
41#include <pwd.h>
42#include <signal.h>
43#include <stdio.h>
44#include <stdlib.h>
45#include <string.h>
46#include <time.h>
47#include <unistd.h>
48#include <sys/types.h>
49#include <sys/stat.h>
50#include <sys/param.h>
51#include <sys/wait.h>
52
53#include "pathnames.h"
54
55#define kbytes(size)  (((size) + 1023) >> 10)
56
57#ifdef _IBMR2
58/* Calculates (db * DEV_BSIZE) */
59#define dbtob(db)  ((unsigned)(db) << UBSHIFT)
60#endif
61
62#define CE_COMPACT 1		/* Compact the achived log files */
63#define CE_BINARY  2		/* Logfile is in binary, don't add */
64				/*  status messages */
65#define	CE_TRIMAT  4		/* trim at a specific time */
66
67#define NONE -1
68
69struct conf_entry {
70	char *log;		/* Name of the log */
71	char *pid_file;		/* PID file */
72	int uid;		/* Owner of log */
73	int gid;		/* Group of log */
74	int numlogs;		/* Number of logs to keep */
75	int size;		/* Size cutoff to trigger trimming the log */
76	int hours;		/* Hours between log trimming */
77	time_t trim_at;		/* Specific time to do trimming */
78	int permissions;	/* File permissions on the log */
79	int flags;		/* Flags (CE_COMPACT & CE_BINARY)  */
80	int sig;		/* Signal to send */
81	struct conf_entry *next;/* Linked list pointer */
82};
83
84int archtodir = 0;		/* Archive old logfiles to other directory */
85int verbose = 0;		/* Print out what's going on */
86int needroot = 1;		/* Root privs are necessary */
87int noaction = 0;		/* Don't do anything, just show it */
88int force = 0;			/* Force the trim no matter what */
89char *archdirname;		/* Directory path to old logfiles archive */
90char *conf = _PATH_CONF;	/* Configuration file to use */
91time_t timenow;
92
93#define MIN_PID         5
94#define MAX_PID		99999	/* was lower, see /usr/include/sys/proc.h */
95char hostname[MAXHOSTNAMELEN];	/* hostname */
96char *daytime;			/* timenow in human readable form */
97
98static struct conf_entry *parse_file(char **files);
99static char *sob(char *p);
100static char *son(char *p);
101static char *missing_field(char *p, char *errline);
102static void do_entry(struct conf_entry * ent);
103static void PRS(int argc, char **argv);
104static void usage();
105static void dotrim(char *log, char *pid_file, int numdays, int falgs, int perm, int owner_uid, int group_gid, int sig);
106static int log_trim(char *log);
107static void compress_log(char *log);
108static int sizefile(char *file);
109static int age_old_log(char *file);
110static pid_t get_pid(char *pid_file);
111static time_t parse8601(char *s);
112static void movefile(char *from, char *to, int perm, int owner_uid, int group_gid);
113static void createdir(char *dirpart);
114static time_t parseDWM(char *s);
115
116int
117main(int argc, char **argv)
118{
119	struct conf_entry *p, *q;
120
121	PRS(argc, argv);
122	if (needroot && getuid() && geteuid())
123		errx(1, "must have root privs");
124	p = q = parse_file(argv + optind);
125
126	while (p) {
127		do_entry(p);
128		p = p->next;
129		free((char *) q);
130		q = p;
131	}
132	return (0);
133}
134
135static void
136do_entry(struct conf_entry * ent)
137{
138	int size, modtime;
139	char *pid_file;
140
141	if (verbose) {
142		if (ent->flags & CE_COMPACT)
143			printf("%s <%dZ>: ", ent->log, ent->numlogs);
144		else
145			printf("%s <%d>: ", ent->log, ent->numlogs);
146	}
147	size = sizefile(ent->log);
148	modtime = age_old_log(ent->log);
149	if (size < 0) {
150		if (verbose)
151			printf("does not exist.\n");
152	} else {
153		if (ent->flags & CE_TRIMAT) {
154			if (timenow < ent->trim_at
155			    || difftime(timenow, ent->trim_at) >= 60 * 60) {
156				if (verbose)
157					printf("--> will trim at %s",
158					    ctime(&ent->trim_at));
159				return;
160			} else if (verbose && ent->hours <= 0) {
161				printf("--> time is up\n");
162			}
163		}
164		if (verbose && (ent->size > 0))
165			printf("size (Kb): %d [%d] ", size, ent->size);
166		if (verbose && (ent->hours > 0))
167			printf(" age (hr): %d [%d] ", modtime, ent->hours);
168		if (force || ((ent->size > 0) && (size >= ent->size)) ||
169		    (ent->hours <= 0 && (ent->flags & CE_TRIMAT)) ||
170		    ((ent->hours > 0) && ((modtime >= ent->hours)
171			    || (modtime < 0)))) {
172			if (verbose)
173				printf("--> trimming log....\n");
174			if (noaction && !verbose) {
175				if (ent->flags & CE_COMPACT)
176					printf("%s <%dZ>: trimming\n",
177					    ent->log, ent->numlogs);
178				else
179					printf("%s <%d>: trimming\n",
180					    ent->log, ent->numlogs);
181			}
182			if (ent->pid_file) {
183				pid_file = ent->pid_file;
184			} else {
185				/* Only try to notify syslog if we are root */
186				if (needroot)
187					pid_file = _PATH_SYSLOGPID;
188				else
189					pid_file = NULL;
190			}
191			dotrim(ent->log, pid_file, ent->numlogs,
192			    ent->flags, ent->permissions, ent->uid, ent->gid, ent->sig);
193		} else {
194			if (verbose)
195				printf("--> skipping\n");
196		}
197	}
198}
199
200static void
201PRS(int argc, char **argv)
202{
203	int c;
204	char *p;
205
206	timenow = time((time_t *) 0);
207	daytime = ctime(&timenow) + 4;
208	daytime[15] = '\0';
209
210	/* Let's get our hostname */
211	(void) gethostname(hostname, sizeof(hostname));
212
213	/* Truncate domain */
214	if ((p = strchr(hostname, '.'))) {
215		*p = '\0';
216	}
217	while ((c = getopt(argc, argv, "nrvFf:a:t:")) != -1)
218		switch (c) {
219		case 'n':
220			noaction++;
221			break;
222		case 'a':
223			archtodir++;
224			archdirname = optarg;
225			break;
226		case 'r':
227			needroot = 0;
228			break;
229		case 'v':
230			verbose++;
231			break;
232		case 'f':
233			conf = optarg;
234			break;
235		case 'F':
236			force++;
237			break;
238		default:
239			usage();
240		}
241}
242
243static void
244usage(void)
245{
246	fprintf(stderr, "usage: newsyslog [-Fnrv] [-f config-file] [-a directory]\n");
247	exit(1);
248}
249
250/*
251 * Parse a configuration file and return a linked list of all the logs to
252 * process
253 */
254static struct conf_entry *
255parse_file(char **files)
256{
257	FILE *f;
258	char line[BUFSIZ], *parse, *q;
259	char *errline, *group;
260	char **p;
261	struct conf_entry *first = NULL;
262	struct conf_entry *working = NULL;
263	struct passwd *pass;
264	struct group *grp;
265	int eol;
266
267	if (strcmp(conf, "-"))
268		f = fopen(conf, "r");
269	else
270		f = stdin;
271	if (!f)
272		err(1, "%s", conf);
273	while (fgets(line, BUFSIZ, f)) {
274		if ((line[0] == '\n') || (line[0] == '#'))
275			continue;
276		errline = strdup(line);
277
278		q = parse = missing_field(sob(line), errline);
279		parse = son(line);
280		if (!*parse)
281			errx(1, "malformed line (missing fields):\n%s", errline);
282		*parse = '\0';
283
284		if (*files) {
285			for (p = files; *p; ++p)
286				if (strcmp(*p, q) == 0)
287					break;
288			if (!*p)
289				continue;
290		}
291
292		if (!first) {
293			if ((working = (struct conf_entry *) malloc(sizeof(struct conf_entry))) == NULL)
294				err(1, "malloc");
295			first = working;
296		} else {
297			if ((working->next = (struct conf_entry *) malloc(sizeof(struct conf_entry))) == NULL)
298				err(1, "malloc");
299			working = working->next;
300		}
301		if ((working->log = strdup(q)) == NULL)
302			err(1, "strdup");
303
304		q = parse = missing_field(sob(++parse), errline);
305		parse = son(parse);
306		if (!*parse)
307			errx(1, "malformed line (missing fields):\n%s", errline);
308		*parse = '\0';
309		if ((group = strchr(q, ':')) != NULL ||
310		    (group = strrchr(q, '.')) != NULL) {
311			*group++ = '\0';
312			if (*q) {
313				if (!(isnumber(*q))) {
314					if ((pass = getpwnam(q)) == NULL)
315						errx(1,
316						    "error in config file; unknown user:\n%s",
317						    errline);
318					working->uid = pass->pw_uid;
319				} else
320					working->uid = atoi(q);
321			} else
322				working->uid = NONE;
323
324			q = group;
325			if (*q) {
326				if (!(isnumber(*q))) {
327					if ((grp = getgrnam(q)) == NULL)
328						errx(1,
329						    "error in config file; unknown group:\n%s",
330						    errline);
331					working->gid = grp->gr_gid;
332				} else
333					working->gid = atoi(q);
334			} else
335				working->gid = NONE;
336
337			q = parse = missing_field(sob(++parse), errline);
338			parse = son(parse);
339			if (!*parse)
340				errx(1, "malformed line (missing fields):\n%s", errline);
341			*parse = '\0';
342		} else
343			working->uid = working->gid = NONE;
344
345		if (!sscanf(q, "%o", &working->permissions))
346			errx(1, "error in config file; bad permissions:\n%s",
347			    errline);
348
349		q = parse = missing_field(sob(++parse), errline);
350		parse = son(parse);
351		if (!*parse)
352			errx(1, "malformed line (missing fields):\n%s", errline);
353		*parse = '\0';
354		if (!sscanf(q, "%d", &working->numlogs))
355			errx(1, "error in config file; bad number:\n%s",
356			    errline);
357
358		q = parse = missing_field(sob(++parse), errline);
359		parse = son(parse);
360		if (!*parse)
361			errx(1, "malformed line (missing fields):\n%s", errline);
362		*parse = '\0';
363		if (isdigit(*q))
364			working->size = atoi(q);
365		else
366			working->size = -1;
367
368		working->flags = 0;
369		q = parse = missing_field(sob(++parse), errline);
370		parse = son(parse);
371		eol = !*parse;
372		*parse = '\0';
373		{
374			char *ep;
375			u_long ul;
376
377			ul = strtoul(q, &ep, 10);
378			if (ep == q)
379				working->hours = 0;
380			else if (*ep == '*')
381				working->hours = -1;
382			else if (ul > INT_MAX)
383				errx(1, "interval is too large:\n%s", errline);
384			else
385				working->hours = ul;
386
387			if (*ep != '\0' && *ep != '@' && *ep != '*' && *ep != '$')
388				errx(1, "malformed interval/at:\n%s", errline);
389			if (*ep == '@') {
390				if ((working->trim_at = parse8601(ep + 1))
391				    == (time_t) - 1)
392					errx(1, "malformed at:\n%s", errline);
393				working->flags |= CE_TRIMAT;
394			} else if (*ep == '$') {
395				if ((working->trim_at = parseDWM(ep + 1))
396				    == (time_t) - 1)
397					errx(1, "malformed at:\n%s", errline);
398				working->flags |= CE_TRIMAT;
399			}
400		}
401
402		if (eol)
403			q = NULL;
404		else {
405			q = parse = sob(++parse);	/* Optional field */
406			parse = son(parse);
407			if (!*parse)
408				eol = 1;
409			*parse = '\0';
410		}
411
412		while (q && *q && !isspace(*q)) {
413			if ((*q == 'Z') || (*q == 'z'))
414				working->flags |= CE_COMPACT;
415			else if ((*q == 'B') || (*q == 'b'))
416				working->flags |= CE_BINARY;
417			else if (*q != '-')
418				errx(1, "illegal flag in config file -- %c", *q);
419			q++;
420		}
421
422		if (eol)
423			q = NULL;
424		else {
425			q = parse = sob(++parse);	/* Optional field */
426			parse = son(parse);
427			if (!*parse)
428				eol = 1;
429			*parse = '\0';
430		}
431
432		working->pid_file = NULL;
433		if (q && *q) {
434			if (*q == '/')
435				working->pid_file = strdup(q);
436			else if (isdigit(*q))
437				goto got_sig;
438			else
439				errx(1, "illegal pid file or signal number in config file:\n%s", errline);
440		}
441		if (eol)
442			q = NULL;
443		else {
444			q = parse = sob(++parse);	/* Optional field */
445			*(parse = son(parse)) = '\0';
446		}
447
448		working->sig = SIGHUP;
449		if (q && *q) {
450			if (isdigit(*q)) {
451		got_sig:
452				working->sig = atoi(q);
453			} else {
454		err_sig:
455				errx(1, "illegal signal number in config file:\n%s", errline);
456			}
457			if (working->sig < 1 || working->sig >= NSIG)
458				goto err_sig;
459		}
460		free(errline);
461	}
462	if (working)
463		working->next = (struct conf_entry *) NULL;
464	(void) fclose(f);
465	return (first);
466}
467
468static char *
469missing_field(char *p, char *errline)
470{
471	if (!p || !*p)
472		errx(1, "missing field in config file:\n%s", errline);
473	return (p);
474}
475
476static void
477dotrim(char *log, char *pid_file, int numdays, int flags, int perm,
478    int owner_uid, int group_gid, int sig)
479{
480	char dirpart[MAXPATHLEN], namepart[MAXPATHLEN];
481	char file1[MAXPATHLEN], file2[MAXPATHLEN];
482	char zfile1[MAXPATHLEN], zfile2[MAXPATHLEN];
483	int notified, need_notification, fd, _numdays;
484	struct stat st;
485	pid_t pid;
486
487#ifdef _IBMR2
488	/*
489	 * AIX 3.1 has a broken fchown- if the owner_uid is -1, it will
490	 * actually change it to be owned by uid -1, instead of leaving it
491	 * as is, as it is supposed to.
492	 */
493	if (owner_uid == -1)
494		owner_uid = geteuid();
495#endif
496
497	if (archtodir) {
498		char *p;
499
500		/* build complete name of archive directory into dirpart */
501		if (*archdirname == '/') {	/* absolute */
502			strlcpy(dirpart, archdirname, sizeof(dirpart));
503		} else {	/* relative */
504			/* get directory part of logfile */
505			strlcpy(dirpart, log, sizeof(dirpart));
506			if ((p = rindex(dirpart, '/')) == NULL)
507				dirpart[0] = '\0';
508			else
509				*(p + 1) = '\0';
510			strlcat(dirpart, archdirname, sizeof(dirpart));
511		}
512
513		/* check if archive directory exists, if not, create it */
514		if (lstat(dirpart, &st))
515			createdir(dirpart);
516
517		/* get filename part of logfile */
518		if ((p = rindex(log, '/')) == NULL)
519			strlcpy(namepart, log, sizeof(namepart));
520		else
521			strlcpy(namepart, p + 1, sizeof(namepart));
522
523		/* name of oldest log */
524		(void) snprintf(file1, sizeof(file1), "%s/%s.%d", dirpart, namepart, numdays);
525		(void) snprintf(zfile1, sizeof(zfile1), "%s%s", file1,
526		    COMPRESS_POSTFIX);
527	} else {
528		/* name of oldest log */
529		(void) snprintf(file1, sizeof(file1), "%s.%d", log, numdays);
530		(void) snprintf(zfile1, sizeof(zfile1), "%s%s", file1,
531		    COMPRESS_POSTFIX);
532	}
533
534	if (noaction) {
535		printf("rm -f %s\n", file1);
536		printf("rm -f %s\n", zfile1);
537	} else {
538		(void) unlink(file1);
539		(void) unlink(zfile1);
540	}
541
542	/* Move down log files */
543	_numdays = numdays;	/* preserve */
544	while (numdays--) {
545
546		(void) strlcpy(file2, file1, sizeof(file2));
547
548		if (archtodir)
549			(void) snprintf(file1, sizeof(file1), "%s/%s.%d", dirpart, namepart, numdays);
550		else
551			(void) snprintf(file1, sizeof(file1), "%s.%d", log, numdays);
552
553		(void) strlcpy(zfile1, file1, sizeof(zfile1));
554		(void) strlcpy(zfile2, file2, sizeof(zfile2));
555		if (lstat(file1, &st)) {
556			(void) strlcat(zfile1, COMPRESS_POSTFIX, sizeof(zfile1));
557			(void) strlcat(zfile2, COMPRESS_POSTFIX, sizeof(zfile2));
558			if (lstat(zfile1, &st))
559				continue;
560		}
561		if (noaction) {
562			printf("mv %s %s\n", zfile1, zfile2);
563			printf("chmod %o %s\n", perm, zfile2);
564			printf("chown %d.%d %s\n",
565			    owner_uid, group_gid, zfile2);
566		} else {
567			(void) rename(zfile1, zfile2);
568			(void) chmod(zfile2, perm);
569			(void) chown(zfile2, owner_uid, group_gid);
570		}
571	}
572	if (!noaction && !(flags & CE_BINARY))
573		(void) log_trim(log);	/* Report the trimming to the old log */
574
575	if (!_numdays) {
576		if (noaction)
577			printf("rm %s\n", log);
578		else
579			(void) unlink(log);
580	} else {
581		if (noaction)
582			printf("mv %s to %s\n", log, file1);
583		else {
584			if (archtodir)
585				movefile(log, file1, perm, owner_uid, group_gid);
586			else
587				(void) rename(log, file1);
588		}
589	}
590
591	if (noaction)
592		printf("Start new log...");
593	else {
594		fd = creat(log, perm);
595		if (fd < 0)
596			err(1, "can't start new log");
597		if (fchown(fd, owner_uid, group_gid))
598			err(1, "can't chmod new log file");
599		(void) close(fd);
600		if (!(flags & CE_BINARY))
601			if (log_trim(log))	/* Add status message */
602				err(1, "can't add status message to log");
603	}
604	if (noaction)
605		printf("chmod %o %s...\n", perm, log);
606	else
607		(void) chmod(log, perm);
608
609	pid = 0;
610	need_notification = notified = 0;
611	if (pid_file != NULL) {
612		need_notification = 1;
613		pid = get_pid(pid_file);
614	}
615	if (pid) {
616		if (noaction) {
617			notified = 1;
618			printf("kill -%d %d\n", sig, (int) pid);
619		} else if (kill(pid, sig))
620			warn("can't notify daemon, pid %d", (int) pid);
621		else {
622			notified = 1;
623			if (verbose)
624				printf("daemon pid %d notified\n", (int) pid);
625		}
626	}
627	if ((flags & CE_COMPACT)) {
628		if (need_notification && !notified)
629			warnx("log %s not compressed because daemon not notified", log);
630		else if (noaction)
631			printf("Compress %s.0\n", log);
632		else {
633			if (notified) {
634				if (verbose)
635					printf("small pause to allow daemon to close log\n");
636				sleep(10);
637			}
638			if (archtodir) {
639				(void) snprintf(file1, sizeof(file1), "%s/%s", dirpart, namepart);
640				compress_log(file1);
641			} else {
642				compress_log(log);
643			}
644		}
645	}
646}
647
648/* Log the fact that the logs were turned over */
649static int
650log_trim(char *log)
651{
652	FILE *f;
653
654	if ((f = fopen(log, "a")) == NULL)
655		return (-1);
656	fprintf(f, "%s %s newsyslog[%d]: logfile turned over\n",
657	    daytime, hostname, (int) getpid());
658	if (fclose(f) == EOF)
659		err(1, "log_trim: fclose:");
660	return (0);
661}
662
663/* Fork of gzip to compress the old log file */
664static void
665compress_log(char *log)
666{
667	pid_t pid;
668	char tmp[MAXPATHLEN];
669
670	(void) snprintf(tmp, sizeof(tmp), "%s.0", log);
671	pid = fork();
672	if (pid < 0)
673		err(1, "fork");
674	else if (!pid) {
675		(void) execl(_PATH_GZIP, _PATH_GZIP, "-f", tmp, 0);
676		err(1, _PATH_GZIP);
677	}
678}
679
680/* Return size in kilobytes of a file */
681static int
682sizefile(char *file)
683{
684	struct stat sb;
685
686	if (stat(file, &sb) < 0)
687		return (-1);
688	return (kbytes(dbtob(sb.st_blocks)));
689}
690
691/* Return the age of old log file (file.0) */
692static int
693age_old_log(char *file)
694{
695	struct stat sb;
696	char tmp[MAXPATHLEN + sizeof(".0") + sizeof(COMPRESS_POSTFIX) + 1];
697
698	if (archtodir) {
699		char *p;
700
701		/* build name of archive directory into tmp */
702		if (*archdirname == '/') {	/* absolute */
703			strlcpy(tmp, archdirname, sizeof(tmp));
704		} else {	/* relative */
705			/* get directory part of logfile */
706			strlcpy(tmp, file, sizeof(tmp));
707			if ((p = rindex(tmp, '/')) == NULL)
708				tmp[0] = '\0';
709			else
710				*(p + 1) = '\0';
711			strlcat(tmp, archdirname, sizeof(tmp));
712		}
713
714		strlcat(tmp, "/", sizeof(tmp));
715
716		/* get filename part of logfile */
717		if ((p = rindex(file, '/')) == NULL)
718			strlcat(tmp, file, sizeof(tmp));
719		else
720			strlcat(tmp, p + 1, sizeof(tmp));
721	} else {
722		(void) strlcpy(tmp, file, sizeof(tmp));
723	}
724
725	if (stat(strcat(tmp, ".0"), &sb) < 0)
726		if (stat(strcat(tmp, COMPRESS_POSTFIX), &sb) < 0)
727			return (-1);
728	return ((int) (timenow - sb.st_mtime + 1800) / 3600);
729}
730
731static pid_t
732get_pid(char *pid_file)
733{
734	FILE *f;
735	char line[BUFSIZ];
736	pid_t pid = 0;
737
738	if ((f = fopen(pid_file, "r")) == NULL)
739		warn("can't open %s pid file to restart a daemon",
740		    pid_file);
741	else {
742		if (fgets(line, BUFSIZ, f)) {
743			pid = atol(line);
744			if (pid < MIN_PID || pid > MAX_PID) {
745				warnx("preposterous process number: %d", (int) pid);
746				pid = 0;
747			}
748		} else
749			warn("can't read %s pid file to restart a daemon",
750			    pid_file);
751		(void) fclose(f);
752	}
753	return pid;
754}
755
756/* Skip Over Blanks */
757char *
758sob(char *p)
759{
760	while (p && *p && isspace(*p))
761		p++;
762	return (p);
763}
764
765/* Skip Over Non-Blanks */
766char *
767son(char *p)
768{
769	while (p && *p && !isspace(*p))
770		p++;
771	return (p);
772}
773
774/*
775 * Parse a limited subset of ISO 8601. The specific format is as follows:
776 *
777 * [CC[YY[MM[DD]]]][THH[MM[SS]]]	(where `T' is the literal letter)
778 *
779 * We don't accept a timezone specification; missing fields (including timezone)
780 * are defaulted to the current date but time zero.
781 */
782static time_t
783parse8601(char *s)
784{
785	char *t;
786	struct tm tm, *tmp;
787	u_long ul;
788
789	tmp = localtime(&timenow);
790	tm = *tmp;
791
792	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
793
794	ul = strtoul(s, &t, 10);
795	if (*t != '\0' && *t != 'T')
796		return -1;
797
798	/*
799	 * Now t points either to the end of the string (if no time was
800	 * provided) or to the letter `T' which separates date and time in
801	 * ISO 8601.  The pointer arithmetic is the same for either case.
802	 */
803	switch (t - s) {
804	case 8:
805		tm.tm_year = ((ul / 1000000) - 19) * 100;
806		ul = ul % 1000000;
807	case 6:
808		tm.tm_year = tm.tm_year - (tm.tm_year % 100);
809		tm.tm_year += ul / 10000;
810		ul = ul % 10000;
811	case 4:
812		tm.tm_mon = (ul / 100) - 1;
813		ul = ul % 100;
814	case 2:
815		tm.tm_mday = ul;
816	case 0:
817		break;
818	default:
819		return -1;
820	}
821
822	/* sanity check */
823	if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12
824	    || tm.tm_mday < 1 || tm.tm_mday > 31)
825		return -1;
826
827	if (*t != '\0') {
828		s = ++t;
829		ul = strtoul(s, &t, 10);
830		if (*t != '\0' && !isspace(*t))
831			return -1;
832
833		switch (t - s) {
834		case 6:
835			tm.tm_sec = ul % 100;
836			ul /= 100;
837		case 4:
838			tm.tm_min = ul % 100;
839			ul /= 100;
840		case 2:
841			tm.tm_hour = ul;
842		case 0:
843			break;
844		default:
845			return -1;
846		}
847
848		/* sanity check */
849		if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0
850		    || tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23)
851			return -1;
852	}
853	return mktime(&tm);
854}
855
856/* physically move file */
857static void
858movefile(char *from, char *to, int perm, int owner_uid, int group_gid)
859{
860	FILE *src, *dst;
861	int c;
862
863	if ((src = fopen(from, "r")) == NULL)
864		err(1, "can't fopen %s for reading", from);
865	if ((dst = fopen(to, "w")) == NULL)
866		err(1, "can't fopen %s for writing", to);
867	if (fchown(fileno(dst), owner_uid, group_gid))
868		err(1, "can't fchown %s", to);
869	if (fchmod(fileno(dst), perm))
870		err(1, "can't fchmod %s", to);
871
872	while ((c = getc(src)) != EOF) {
873		if ((putc(c, dst)) == EOF)
874			err(1, "error writing to %s", to);
875	}
876
877	if (ferror(src))
878		err(1, "error reading from %s", from);
879	if ((fclose(src)) != 0)
880		err(1, "can't fclose %s", to);
881	if ((fclose(dst)) != 0)
882		err(1, "can't fclose %s", from);
883	if ((unlink(from)) != 0)
884		err(1, "can't unlink %s", from);
885}
886
887/* create one or more directory components of a path */
888static void
889createdir(char *dirpart)
890{
891	char *s, *d;
892	char mkdirpath[MAXPATHLEN];
893	struct stat st;
894
895	s = dirpart;
896	d = mkdirpath;
897
898	for (;;) {
899		*d++ = *s++;
900		if (*s == '/' || *s == '\0') {
901			*d = '\0';
902			if (lstat(mkdirpath, &st))
903				mkdir(mkdirpath, 0755);
904		}
905		if (*s == '\0')
906			break;
907	}
908}
909
910/*-
911 * Parse a cyclic time specification, the format is as follows:
912 *
913 *	[Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
914 *
915 * to rotate a logfile cyclic at
916 *
917 *	- every day (D) within a specific hour (hh)	(hh = 0...23)
918 *	- once a week (W) at a specific day (d)     OR	(d = 0..6, 0 = Sunday)
919 *	- once a month (M) at a specific day (d)	(d = 1..31,l|L)
920 *
921 * We don't accept a timezone specification; missing fields
922 * are defaulted to the current date but time zero.
923 */
924static time_t
925parseDWM(char *s)
926{
927	char *t;
928	struct tm tm, *tmp;
929	u_long ul;
930	int nd;
931	static int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
932	int WMseen = 0;
933	int Dseen = 0;
934
935	tmp = localtime(&timenow);
936	tm = *tmp;
937
938	/* set no. of days per month */
939
940	nd = mtab[tm.tm_mon];
941
942	if (tm.tm_mon == 1) {
943		if (((tm.tm_year + 1900) % 4 == 0) &&
944		    ((tm.tm_year + 1900) % 100 != 0) &&
945		    ((tm.tm_year + 1900) % 400 == 0)) {
946			nd++;	/* leap year, 29 days in february */
947		}
948	}
949	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
950
951	for (;;) {
952		switch (*s) {
953		case 'D':
954			if (Dseen)
955				return -1;
956			Dseen++;
957			s++;
958			ul = strtoul(s, &t, 10);
959			if (ul < 0 || ul > 23)
960				return -1;
961			tm.tm_hour = ul;
962			break;
963
964		case 'W':
965			if (WMseen)
966				return -1;
967			WMseen++;
968			s++;
969			ul = strtoul(s, &t, 10);
970			if (ul < 0 || ul > 6)
971				return -1;
972			if (ul != tm.tm_wday) {
973				int save;
974
975				if (ul < tm.tm_wday) {
976					save = 6 - tm.tm_wday;
977					save += (ul + 1);
978				} else {
979					save = ul - tm.tm_wday;
980				}
981
982				tm.tm_mday += save;
983
984				if (tm.tm_mday > nd) {
985					tm.tm_mon++;
986					tm.tm_mday = tm.tm_mday - nd;
987				}
988			}
989			break;
990
991		case 'M':
992			if (WMseen)
993				return -1;
994			WMseen++;
995			s++;
996			if (tolower(*s) == 'l') {
997				tm.tm_mday = nd;
998				s++;
999				t = s;
1000			} else {
1001				ul = strtoul(s, &t, 10);
1002				if (ul < 1 || ul > 31)
1003					return -1;
1004
1005				if (ul > nd)
1006					return -1;
1007				tm.tm_mday = ul;
1008			}
1009			break;
1010
1011		default:
1012			return (-1);
1013			break;
1014		}
1015
1016		if (*t == '\0' || isspace(*t))
1017			break;
1018		else
1019			s = t;
1020	}
1021	return mktime(&tm);
1022}
1023