xref: /netbsd/usr.bin/newsyslog/newsyslog.c (revision bf9ec67e)
1 /*	$NetBSD: newsyslog.c,v 1.43 2002/02/11 10:57:58 wiz Exp $	*/
2 
3 /*
4  * Copyright (c) 1999, 2000 Andrew Doran <ad@NetBSD.org>
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  *
16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26  * SUCH DAMAGE.
27  *
28  */
29 
30 /*
31  * This file contains changes from the Open Software Foundation.
32  */
33 
34 /*
35  * Copyright 1988, 1989 by the Massachusetts Institute of Technology
36  *
37  * Permission to use, copy, modify, and distribute this software
38  * and its documentation for any purpose and without fee is
39  * hereby granted, provided that the above copyright notice
40  * appear in all copies and that both that copyright notice and
41  * this permission notice appear in supporting documentation,
42  * and that the names of M.I.T. and the M.I.T. S.I.P.B. not be
43  * used in advertising or publicity pertaining to distribution
44  * of the software without specific, written prior permission.
45  * M.I.T. and the M.I.T. S.I.P.B. make no representations about
46  * the suitability of this software for any purpose.  It is
47  * provided "as is" without express or implied warranty.
48  *
49  */
50 
51 /*
52  * newsyslog(8) - a program to roll over log files provided that specified
53  * critera are met, optionally preserving a number of historical log files.
54  */
55 
56 #include <sys/cdefs.h>
57 #ifndef lint
58 __RCSID("$NetBSD: newsyslog.c,v 1.43 2002/02/11 10:57:58 wiz Exp $");
59 #endif /* not lint */
60 
61 #include <sys/types.h>
62 #include <sys/time.h>
63 #include <sys/stat.h>
64 #include <sys/param.h>
65 #include <sys/wait.h>
66 
67 #include <ctype.h>
68 #include <fcntl.h>
69 #include <grp.h>
70 #include <pwd.h>
71 #include <signal.h>
72 #include <stdio.h>
73 #include <stdlib.h>
74 #include <stdarg.h>
75 #include <string.h>
76 #include <time.h>
77 #include <unistd.h>
78 #include <errno.h>
79 #include <err.h>
80 #include <util.h>
81 #include <paths.h>
82 
83 #include "pathnames.h"
84 
85 #define	PRHDRINFO(x)	((void)(verbose ? printf x : 0))
86 #define	PRINFO(x)	((void)(verbose ? printf("  ") + printf x : 0))
87 
88 #define	CE_COMPRESS	0x01	/* Compress the archived log files */
89 #define	CE_BINARY	0x02	/* Logfile is a binary file/non-syslog */
90 #define	CE_NOSIGNAL	0x04	/* Don't send a signal when trimmed */
91 #define	CE_CREATE	0x08	/* Create log file if none exists */
92 #define	CE_PLAIN0	0x10	/* Do not compress zero'th history file */
93 
94 struct conf_entry {
95 	uid_t	uid;			/* Owner of log */
96 	gid_t	gid;			/* Group of log */
97 	mode_t	mode;			/* File permissions */
98 	int	numhist;		/* Number of historical logs to keep */
99 	size_t	maxsize;		/* Maximum log size */
100 	int	maxage;			/* Hours between log trimming */
101 	time_t	trimat;			/* Specific trim time */
102 	int	flags;			/* Flags (CE_*) */
103 	int	signum;			/* Signal to send */
104 	char	pidfile[MAXPATHLEN];	/* File containing PID to signal */
105 	char	logfile[MAXPATHLEN];	/* Path to log file */
106 };
107 
108 int	verbose;			/* Be verbose */
109 int	noaction;			/* Take no action */
110 int	nosignal;			/* Do not send signals */
111 char    hostname[MAXHOSTNAMELEN + 1];	/* Hostname, stripped of domain */
112 uid_t	myeuid;				/* EUID we are running with */
113 
114 int	getsig(const char *);
115 int	isnumber(const char *);
116 int	main(int, char **);
117 int	parse_cfgline(struct conf_entry *, FILE *, size_t *);
118 time_t	parse_iso8601(char *);
119 time_t	parse_dwm(char *);
120 int	parse_userspec(const char *, struct passwd **, struct group **);
121 pid_t	readpidfile(const char *);
122 void	usage(void);
123 
124 void	log_compress(struct conf_entry *, const char *);
125 void	log_create(struct conf_entry *);
126 void	log_examine(struct conf_entry *, int);
127 void	log_trim(struct conf_entry *);
128 void	log_trimmed(struct conf_entry *);
129 
130 /*
131  * Program entry point.
132  */
133 int
134 main(int argc, char **argv)
135 {
136 	struct conf_entry log;
137 	FILE *fd;
138 	char *p, *cfile;
139 	int c, needroot, i, force;
140 	size_t lineno;
141 
142 	force = 0;
143 	needroot = 1;
144 	cfile = _PATH_NEWSYSLOGCONF;
145 
146 	gethostname(hostname, sizeof (hostname));
147 	hostname[sizeof (hostname) - 1] = '\0';
148 
149 	/* Truncate domain. */
150 	if ((p = strchr(hostname, '.')) != NULL)
151 		*p = '\0';
152 
153 	/* Parse command line options. */
154 	while ((c = getopt(argc, argv, "f:nrsvF")) != -1) {
155 		switch (c) {
156 		case 'f':
157 			cfile = optarg;
158 			break;
159 		case 'n':
160 			noaction = 1;
161 			verbose = 1;
162 			break;
163 		case 'r':
164 			needroot = 0;
165 			break;
166 		case 's':
167 			nosignal = 1;
168 			break;
169 		case 'v':
170 			verbose = 1;
171 			break;
172 		case 'F':
173 			force = 1;
174 			break;
175 		default:
176 			usage();
177 			/* NOTREACHED */
178 		}
179 	}
180 
181 	myeuid = geteuid();
182 	if (needroot && myeuid != 0)
183 		errx(EXIT_FAILURE, "must be run as root");
184 
185 	argc -= optind;
186 	argv += optind;
187 
188 	if (strcmp(cfile, "-") == 0)
189 		fd = stdin;
190 	else if ((fd = fopen(cfile, "rt")) == NULL)
191 		err(EXIT_FAILURE, "%s", cfile);
192 
193 	for (lineno = 0; !parse_cfgline(&log, fd, &lineno);) {
194 		/*
195 		 * If specific log files were specified, touch only
196 		 * those.
197 		 */
198 		if (argc != 0) {
199 			for (i = 0; i < argc; i++)
200 				if (strcmp(log.logfile, argv[i]) == 0)
201 					break;
202 			if (i == argc)
203 				continue;
204 		}
205 		log_examine(&log, force);
206 	}
207 
208 	if (fd != stdin)
209 		fclose(fd);
210 
211 	exit(EXIT_SUCCESS);
212 	/* NOTREACHED */
213 }
214 
215 /*
216  * Parse a single line from the configuration file.
217  */
218 int
219 parse_cfgline(struct conf_entry *log, FILE *fd, size_t *_lineno)
220 {
221 	char *line, *q, **ap, *argv[10];
222 	struct passwd *pw;
223 	struct group *gr;
224 	int nf, lineno, i, rv;
225 
226 	rv = -1;
227 	line = NULL;
228 
229 	/* Place the white-space separated fields into an array. */
230 	do {
231 		if (line != NULL)
232 			free(line);
233 		if ((line = fparseln(fd, NULL, _lineno, NULL, 0)) == NULL)
234 			return (rv);
235 		lineno = (int)*_lineno;
236 
237 		for (ap = argv, nf = 0; (*ap = strsep(&line, " \t")) != NULL;)
238 			if (**ap != '\0') {
239 				if (++nf == sizeof (argv) / sizeof (argv[0])) {
240 					warnx("config line %d: "
241 					    "too many fields", lineno);
242 					goto bad;
243 				}
244 				ap++;
245 			}
246 	} while (nf == 0);
247 
248 	if (nf < 6)
249 		errx(EXIT_FAILURE, "config line %d: too few fields", lineno);
250 
251 	memset(log, 0, sizeof (*log));
252 
253 	/* logfile_name */
254 	ap = argv;
255 	strlcpy(log->logfile, *ap++, sizeof (log->logfile));
256 	if (log->logfile[0] != '/')
257 		errx(EXIT_FAILURE,
258 		    "config line %d: logfile must have a full path", lineno);
259 
260 	/* owner:group */
261 	if (strchr(*ap, ':') != NULL || strchr(*ap, '.') != NULL) {
262 		if (parse_userspec(*ap++, &pw, &gr)) {
263 			warnx("config line %d: unknown user/group", lineno);
264 			goto bad;
265 		}
266 
267 		/*
268 		 * We may only change the file's owner as non-root.
269 		 */
270 		if (myeuid != 0) {
271 			if (pw->pw_uid != myeuid)
272 				errx(EXIT_FAILURE, "config line %d: user:group "
273 				    "as non-root must match current user",
274 				    lineno);
275 			log->uid = (uid_t)-1;
276 		} else
277 			log->uid = pw->pw_uid;
278 		log->gid = gr->gr_gid;
279 		if (nf < 7)
280 			errx(EXIT_FAILURE, "config line %d: too few fields",
281 			    lineno);
282 	} else if (myeuid != 0) {
283 		log->uid = (uid_t)-1;
284 		log->gid = getegid();
285 	}
286 
287 	/* mode */
288 	if (sscanf(*ap++, "%o", &i) != 1) {
289 		warnx("config line %d: bad permissions", lineno);
290 		goto bad;
291 	}
292 	log->mode = (mode_t)i;
293 
294 	/* count */
295 	if (sscanf(*ap++, "%d", &log->numhist) != 1) {
296 		warnx("config line %d: bad log count", lineno);
297 		goto bad;
298 	}
299 
300 	/* size */
301 	if (isdigit(**ap)) {
302 		log->maxsize = (int)strtol(*ap, &q, 0);
303 		if (*q != '\0') {
304 			warnx("config line %d: bad log size", lineno);
305 			goto bad;
306 		}
307 	} else if (**ap == '*')
308 		log->maxsize = (size_t)-1;
309 	else {
310 		warnx("config line %d: bad log size", lineno);
311 		goto bad;
312 	}
313 	ap++;
314 
315 	/* when */
316 	log->maxage = -1;
317 	log->trimat = (time_t)-1;
318 	q = *ap++;
319 
320 	if (strcmp(q, "*") != 0) {
321 		if (isdigit(*q))
322 			log->maxage = (int)strtol(q, &q, 10);
323 
324 		/*
325 		 * One class of periodic interval specification can follow a
326 		 * maximum age specification.  Handle it.
327 		 */
328 		if (*q == '@') {
329 			log->trimat = parse_iso8601(q + 1);
330 			if (log->trimat == (time_t)-1) {
331 				warnx("config line %d: bad trim time", lineno);
332 				goto bad;
333 			}
334 		} else if (*q == '$') {
335 			if ((log->trimat = parse_dwm(q + 1)) == (time_t)-1) {
336 				warnx("config line %d: bad trim time", lineno);
337 				goto bad;
338 			}
339 		} else if (log->maxage == -1) {
340 			warnx("config line %d: bad log age", lineno);
341 			goto bad;
342 		}
343 	}
344 
345 	/* flags */
346 	log->flags = (nosignal ? CE_NOSIGNAL : 0);
347 
348 	for (q = *ap++; q != NULL && *q != '\0'; q++) {
349 		switch (tolower(*q)) {
350 		case 'b':
351 			log->flags |= CE_BINARY;
352 			break;
353 		case 'c':
354 			log->flags |= CE_CREATE;
355 			break;
356 		case 'n':
357 			log->flags |= CE_NOSIGNAL;
358 			break;
359 		case 'p':
360 			log->flags |= CE_PLAIN0;
361 			break;
362 		case 'z':
363 			log->flags |= CE_COMPRESS;
364 			break;
365 		case '-':
366 			break;
367 		default:
368 			warnx("config line %d: bad flags", lineno);
369 			goto bad;
370 		}
371 	}
372 
373 	/* path_to_pidfile */
374 	if (*ap != NULL && **ap == '/')
375 		strlcpy(log->pidfile, *ap++, sizeof (log->pidfile));
376 	else
377 		log->pidfile[0] = '\0';
378 
379 	/* sigtype */
380 	if (*ap != NULL) {
381 		if ((log->signum = getsig(*ap++)) < 0) {
382 			warnx("config line %d: bad signal type", lineno);
383 			goto bad;
384 		}
385 	} else
386 		log->signum = SIGHUP;
387 
388 	rv = 0;
389 
390 bad:
391 	free(line);
392 	return (rv);
393 }
394 
395 /*
396  * Examine a log file.  If the trim conditions are met, call log_trim() to
397  * trim the log file.
398  */
399 void
400 log_examine(struct conf_entry *log, int force)
401 {
402 	struct stat sb;
403 	size_t size;
404 	int age, trim;
405 	char tmp[MAXPATHLEN];
406 	const char *reason;
407 	time_t now;
408 
409 	now = time(NULL);
410 
411 	PRHDRINFO(("\n%s <%d%s>: ", log->logfile, log->numhist,
412 	    (log->flags & CE_COMPRESS) != 0 ? "Z" : ""));
413 
414 	/*
415 	 * stat() the logfile.  If it doesn't exist and the `c' flag has
416 	 * been specified, create it.  If it doesn't exist and the `c' flag
417 	 * hasn't been specified, give up.
418 	 */
419 	if (stat(log->logfile, &sb) < 0) {
420 		if (errno == ENOENT && (log->flags & CE_CREATE) != 0) {
421 			PRHDRINFO(("creating; "));
422 			if (!noaction)
423 				log_create(log);
424 			else {
425 				PRHDRINFO(("can't proceed with `-n'\n"));
426 				return;
427 			}
428 			if (stat(log->logfile, &sb))
429 				err(EXIT_FAILURE, "%s", log->logfile);
430 		} else if (errno == ENOENT) {
431 			PRHDRINFO(("does not exist --> skip log\n"));
432 			return;
433 		} else if (errno != 0)
434 			err(EXIT_FAILURE, "%s", log->logfile);
435 	}
436 
437 	/* Size of the log file in kB. */
438 	size = ((size_t)sb.st_blocks * S_BLKSIZE) >> 10;
439 
440 	/*
441 	 * Get the age (expressed in hours) of the current log file with
442 	 * respect to the newest historical log file.
443 	 */
444 	strlcpy(tmp, log->logfile, sizeof (tmp));
445 	strlcat(tmp, ".0", sizeof (tmp));
446 	if (stat(tmp, &sb) < 0) {
447 		strlcat(tmp, ".gz", sizeof (tmp));
448 		if (stat(tmp, &sb) < 0)
449 			age = -1;
450 		else
451 			age = (int)(now - sb.st_mtime + 1800) / 3600;
452 	} else
453 		age = (int)(now - sb.st_mtime + 1800) / 3600;
454 
455 	/*
456 	 * Examine the set of given trim conditions and if any one is met,
457 	 * trim the log.
458 	 *
459 	 * Note: if `maxage' or `trimat' is used as a trim condition, we
460 	 * need at least one historical log file to determine the `age' of
461 	 * the active log file.  WRT `trimat', we will trim up to one hour
462 	 * after the specific trim time has passed - we need to know if
463 	 * we've trimmed to meet that condition with a previous invocation
464 	 * of newsyslog(8).
465 	 */
466 	if (log->maxage >= 0 && (age >= log->maxage || age < 0)) {
467 		trim = 1;
468 		reason = "log age > interval";
469 	} else if (size >= log->maxsize) {
470 		trim = 1;
471 		reason = "log size > size";
472 	} else if (log->trimat != (time_t)-1 && now >= log->trimat &&
473 		   (age == -1 || age > 1) &&
474 		   difftime(now, log->trimat) < 60 * 60) {
475 		trim = 1;
476 		reason = "specific trim time";
477 	} else {
478 		trim = force;
479 		reason = "trim forced";
480 	}
481 
482 	if (trim) {
483 		PRHDRINFO(("--> trim log (%s)\n", reason));
484 		log_trim(log);
485 	} else
486 		PRHDRINFO(("--> skip log (trim conditions not met)\n"));
487 }
488 
489 /*
490  * Trim the specified log file.
491  */
492 void
493 log_trim(struct conf_entry *log)
494 {
495 	char file1[MAXPATHLEN], file2[MAXPATHLEN];
496 	int i;
497 	struct stat st;
498 	pid_t pid;
499 
500 	/* Remove oldest historical log. */
501 	snprintf(file1, sizeof (file1), "%s.%d", log->logfile,
502 	    log->numhist - 1);
503 
504 	PRINFO(("rm -f %s\n", file1));
505 	if (!noaction)
506 		unlink(file1);
507 	strlcat(file1, ".gz", sizeof (file1));
508 	PRINFO(("rm -f %s\n", file1));
509 	if (!noaction)
510 		unlink(file1);
511 
512 	/* Move down log files. */
513 	for (i = log->numhist - 1; i != 0; i--) {
514 		snprintf(file1, sizeof (file1), "%s.%d", log->logfile, i - 1);
515 		snprintf(file2, sizeof (file2), "%s.%d", log->logfile, i);
516 
517 		if (lstat(file1, &st) != 0) {
518 			strlcat(file1, ".gz", sizeof (file1));
519 			strlcat(file2, ".gz", sizeof (file2));
520 			if (lstat(file1, &st) != 0)
521 				continue;
522 		}
523 
524 		PRINFO(("mv %s %s\n", file1, file2));
525 		if (!noaction)
526 			if (rename(file1, file2))
527 				err(EXIT_FAILURE, "%s", file1);
528 		PRINFO(("chmod %o %s\n", log->mode, file2));
529 		if (!noaction)
530 			if (chmod(file2, log->mode))
531 				err(EXIT_FAILURE, "%s", file2);
532 		PRINFO(("chown %d:%d %s\n", log->uid, log->gid,
533 		    file2));
534 		if (!noaction)
535 			if (chown(file2, log->uid, log->gid))
536 				err(EXIT_FAILURE, "%s", file2);
537 	}
538 
539 	/*
540 	 * If a historical log file isn't compressed, and 'z' has been
541 	 * specified, compress it.  (This is convenient, but is also needed
542 	 * if 'p' has been specified.)  It should be noted that gzip(1)
543 	 * preserves file ownership and file mode.
544 	 */
545 	for (i = (log->flags & CE_PLAIN0) != 0; i < log->numhist; i++) {
546 		snprintf(file1, sizeof (file1), "%s.%d", log->logfile, i);
547 		if (lstat(file1, &st) != 0)
548 			continue;
549 		snprintf(file2, sizeof (file2), "%s.gz", file1);
550 		if (lstat(file2, &st) == 0)
551 			continue;
552 		log_compress(log, file1);
553 	}
554 
555 	log_trimmed(log);
556 
557 	/* Create the historical log file if we're maintaining history. */
558 	if (log->numhist == 0) {
559 		PRINFO(("rm -f %s\n", log->logfile));
560 		if (!noaction)
561 			if (unlink(log->logfile))
562 				err(EXIT_FAILURE, "%s", log->logfile);
563 	} else {
564 		snprintf(file1, sizeof (file1), "%s.0", log->logfile);
565 		PRINFO(("mv %s %s\n", log->logfile, file1));
566 		if (!noaction)
567 			if (rename(log->logfile, file1))
568 				err(EXIT_FAILURE, "%s", log->logfile);
569 	}
570 
571 	PRINFO(("(create new log)\n"));
572 	log_create(log);
573 	log_trimmed(log);
574 
575 	/* Set the correct permissions on the log. */
576 	PRINFO(("chmod %o %s\n", log->mode, log->logfile));
577 	if (!noaction)
578 		if (chmod(log->logfile, log->mode))
579 			err(EXIT_FAILURE, "%s", log->logfile);
580 
581 	/* Do we need to signal a daemon? */
582 	if ((log->flags & CE_NOSIGNAL) == 0) {
583 		if (log->pidfile[0] != '\0')
584 			pid = readpidfile(log->pidfile);
585 		else
586 			pid = readpidfile(_PATH_SYSLOGDPID);
587 
588 		if (pid != (pid_t)-1) {
589 			PRINFO(("kill -%s %lu\n",
590 			    sys_signame[log->signum], (u_long)pid));
591 			if (!noaction)
592 				if (kill(pid, log->signum))
593 					warn("kill");
594 		}
595 	}
596 
597 	/* If the newest historical log is to be compressed, do it here. */
598 	if ((log->flags & (CE_PLAIN0 | CE_COMPRESS)) == CE_COMPRESS) {
599 		snprintf(file1, sizeof (file1), "%s.0", log->logfile);
600 		if ((log->flags & CE_NOSIGNAL) == 0) {
601 			PRINFO(("sleep for 10 seconds before compressing...\n"));
602 			sleep(10);
603 		}
604 		log_compress(log, file1);
605 	}
606 }
607 
608 /*
609  * Write an entry to the log file recording the fact that it was trimmed.
610  */
611 void
612 log_trimmed(struct conf_entry *log)
613 {
614 	FILE *fd;
615 	time_t now;
616 	char *daytime;
617 
618 	if ((log->flags & CE_BINARY) != 0)
619 		return;
620 	PRINFO(("(append rotation notice to %s)\n", log->logfile));
621 	if (noaction)
622 		return;
623 
624 	if ((fd = fopen(log->logfile, "at")) == NULL)
625 		err(EXIT_FAILURE, "%s", log->logfile);
626 
627 	now = time(NULL);
628 	daytime = ctime(&now) + 4;
629 	daytime[15] = '\0';
630 
631 	fprintf(fd, "%s %s newsyslog[%lu]: log file turned over\n", daytime,
632 	    hostname, (u_long)getpid());
633 	fclose(fd);
634 }
635 
636 /*
637  * Create a new log file.
638  */
639 void
640 log_create(struct conf_entry *log)
641 {
642 	int fd;
643 
644 	if (noaction)
645 		return;
646 
647 	if ((fd = creat(log->logfile, log->mode)) < 0)
648 		err(EXIT_FAILURE, "%s", log->logfile);
649 	if (fchown(fd, log->uid, log->gid) < 0)
650 		err(EXIT_FAILURE, "%s", log->logfile);
651 	close(fd);
652 }
653 
654 /*
655  * Fork off gzip(1) to compress a log file.  This routine takes an
656  * additional string argument (the name of the file to compress): it is also
657  * used to compress historical log files other than the newest.
658  */
659 void
660 log_compress(struct conf_entry *log, const char *fn)
661 {
662 	char tmp[MAXPATHLEN];
663 
664 	PRINFO(("gzip %s\n", fn));
665 	if (!noaction) {
666 		pid_t pid;
667 		int status;
668 
669 		if ((pid = vfork()) < 0)
670 			err(EXIT_FAILURE, "vfork");
671 		else if (pid == 0) {
672 			execl(_PATH_GZIP, "gzip", "-f", fn, NULL);
673 			_exit(EXIT_FAILURE);
674 		}
675 		while (waitpid(pid, &status, 0) != pid);
676 
677 		if (!WIFEXITED(status) || (WEXITSTATUS(status) != 0))
678 			errx(EXIT_FAILURE, "gzip failed");
679 	}
680 
681 	snprintf(tmp, sizeof (tmp), "%s.gz", fn);
682 	PRINFO(("chown %d:%d %s\n", log->uid, log->gid, tmp));
683 	if (!noaction)
684 		if (chown(tmp, log->uid, log->gid))
685 			err(EXIT_FAILURE, "%s", tmp);
686 }
687 
688 /*
689  * Display program usage information.
690  */
691 void
692 usage(void)
693 {
694 
695 	fprintf(stderr,
696 	    "usage: newsyslog [-nrsvF] [-f config-file] [file ...]\n");
697 	exit(EXIT_FAILURE);
698 }
699 
700 /*
701  * Return non-zero if a string represents a decimal value.
702  */
703 int
704 isnumber(const char *string)
705 {
706 
707 	while (isdigit(*string))
708 		string++;
709 
710 	return (*string == '\0');
711 }
712 
713 /*
714  * Given a signal name, attempt to find the corresponding signal number.
715  */
716 int
717 getsig(const char *sig)
718 {
719 	char *p;
720 	int n;
721 
722 	if (isnumber(sig)) {
723 		n = (int)strtol(sig, &p, 0);
724 		if (p != '\0' || n < 0 || n >= NSIG)
725 			return (-1);
726 		return (n);
727 	}
728 
729 	if (strncasecmp(sig, "SIG", 3) == 0)
730 		sig += 3;
731 	for (n = 1; n < NSIG; n++)
732 		if (strcasecmp(sys_signame[n], sig) == 0)
733 			return (n);
734 	return (-1);
735 }
736 
737 /*
738  * Given a path to a PID file, return the PID contained within.
739  */
740 pid_t
741 readpidfile(const char *file)
742 {
743 	FILE *fd;
744 	char line[BUFSIZ];
745 	pid_t pid;
746 
747 #ifdef notyet
748 	if (file[0] != '/')
749 		snprintf(tmp, sizeof (tmp), "%s%s", _PATH_VARRUN, file);
750 	else
751 		strlcpy(tmp, file, sizeof (tmp));
752 #endif
753 
754 	if ((fd = fopen(file, "rt")) == NULL) {
755 		warn("%s", file);
756 		return (-1);
757 	}
758 
759 	if (fgets(line, sizeof (line) - 1, fd) != NULL) {
760 		line[sizeof (line) - 1] = '\0';
761 		pid = (pid_t)strtol(line, NULL, 0);
762 	} else {
763 		warnx("unable to read %s", file);
764 		pid = (pid_t)-1;
765 	}
766 
767 	fclose(fd);
768 	return (pid);
769 }
770 
771 /*
772  * Parse a user:group specification.
773  *
774  * XXX This is over the top for newsyslog(8).  It should be moved to libutil.
775  */
776 int
777 parse_userspec(const char *name, struct passwd **pw, struct group **gr)
778 {
779 	char buf[MAXLOGNAME * 2 + 2], *group;
780 
781 	strlcpy(buf, name, sizeof (buf));
782 	*gr = NULL;
783 
784 	/*
785 	 * Before attempting to use '.' as a separator, see if the whole
786 	 * string resolves as a user name.
787 	 */
788 	if ((*pw = getpwnam(buf)) != NULL) {
789 		*gr = getgrgid((*pw)->pw_gid);
790 		return (0);
791 	}
792 
793 	/* Split the user and group name. */
794 	if ((group = strchr(buf, ':')) != NULL ||
795 	    (group = strchr(buf, '.')) != NULL)
796 		*group++ = '\0';
797 
798 	if (isnumber(buf))
799 		*pw = getpwuid((uid_t)atoi(buf));
800 	else
801 		*pw = getpwnam(buf);
802 
803 	/*
804 	 * Find the group.  If a group wasn't specified, use the user's
805 	 * `natural' group.  We get to this point even if no user was found.
806 	 * This is to allow the caller to get a better idea of what went
807 	 * wrong, if anything.
808 	 */
809 	if (group == NULL || *group == '\0') {
810 		if (*pw == NULL)
811 			return (-1);
812 		*gr = getgrgid((*pw)->pw_gid);
813 	} else if (isnumber(group))
814 		*gr = getgrgid((gid_t)atoi(group));
815 	else
816 		*gr = getgrnam(group);
817 
818 	return (*pw != NULL && *gr != NULL ? 0 : -1);
819 }
820 
821 /*
822  * Parse a cyclic time specification, the format is as follows:
823  *
824  *	[Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
825  *
826  * to rotate a log file cyclic at
827  *
828  *	- every day (D) within a specific hour (hh)	(hh = 0...23)
829  *	- once a week (W) at a specific day (d)     OR	(d = 0..6, 0 = Sunday)
830  *	- once a month (M) at a specific day (d)	(d = 1..31,l|L)
831  *
832  * We don't accept a timezone specification; missing fields are defaulted to
833  * the current date but time zero.
834  */
835 time_t
836 parse_dwm(char *s)
837 {
838 	char *t;
839 	struct tm tm, *tmp;
840 	u_long ul;
841 	time_t now;
842 	static int mtab[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
843 	int wmseen, dseen, nd, save;
844 
845 	wmseen = 0;
846 	dseen = 0;
847 
848 	now = time(NULL);
849 	tmp = localtime(&now);
850 	tm = *tmp;
851 
852 	/* Set no. of days per month */
853 	nd = mtab[tm.tm_mon];
854 
855 	if (tm.tm_mon == 1 &&
856 	    ((tm.tm_year + 1900) % 4 == 0) &&
857 	    ((tm.tm_year + 1900) % 100 != 0) &&
858 	    ((tm.tm_year + 1900) % 400 == 0))
859 		nd++;	/* leap year, 29 days in february */
860 	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
861 
862 	for (;;) {
863 		switch (*s) {
864 		case 'D':
865 			if (dseen)
866 				return ((time_t)-1);
867 			dseen++;
868 			s++;
869 			ul = strtoul(s, &t, 10);
870 			if (ul < 0 || ul > 23)
871 				return ((time_t)-1);
872 			tm.tm_hour = ul;
873 			break;
874 
875 		case 'W':
876 			if (wmseen)
877 				return (-1);
878 			wmseen++;
879 			s++;
880 			ul = strtoul(s, &t, 10);
881 			if (ul < 0 || ul > 6)
882 				return (-1);
883 			if (ul != tm.tm_wday) {
884 				if (ul < tm.tm_wday) {
885 					save = 6 - tm.tm_wday;
886 					save += (ul + 1);
887 				} else
888 					save = ul - tm.tm_wday;
889 				tm.tm_mday += save;
890 
891 				if (tm.tm_mday > nd) {
892 					tm.tm_mon++;
893 					tm.tm_mday = tm.tm_mday - nd;
894 				}
895 			}
896 			break;
897 
898 		case 'M':
899 			if (wmseen)
900 				return (-1);
901 			wmseen++;
902 			s++;
903 			if (tolower(*s) == 'l') {
904 				tm.tm_mday = nd;
905 				s++;
906 				t = s;
907 			} else {
908 				ul = strtoul(s, &t, 10);
909 				if (ul < 1 || ul > 31)
910 					return (-1);
911 
912 				if (ul > nd)
913 					return (-1);
914 				tm.tm_mday = ul;
915 			}
916 			break;
917 
918 		default:
919 			return (-1);
920 			break;
921 		}
922 
923 		if (*t == '\0' || isspace(*t))
924 			break;
925 		else
926 			s = t;
927 	}
928 
929 	return (mktime(&tm));
930 }
931 
932 /*
933  * Parse a limited subset of ISO 8601.  The specific format is as follows:
934  *
935  * [CC[YY[MM[DD]]]][THH[MM[SS]]]	(where `T' is the literal letter)
936  *
937  * We don't accept a timezone specification; missing fields (including
938  * timezone) are defaulted to the current date but time zero.
939  */
940 time_t
941 parse_iso8601(char *s)
942 {
943 	char *t;
944 	struct tm tm, *tmp;
945 	u_long ul;
946 	time_t now;
947 
948 	now = time(NULL);
949 	tmp = localtime(&now);
950 	tm = *tmp;
951 
952 	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
953 
954 	ul = strtoul(s, &t, 10);
955 	if (*t != '\0' && *t != 'T')
956 		return ((time_t)-1);
957 
958 	/*
959 	 * Now t points either to the end of the string (if no time was
960 	 * provided) or to the letter `T' which separates date and time in
961 	 * ISO 8601.  The pointer arithmetic is the same for either case.
962 	 */
963 	switch (t - s) {
964 	case 8:
965 		tm.tm_year = ((ul / 1000000) - 19) * 100;
966 		ul = ul % 1000000;
967 		/* FALLTHROUGH */
968 	case 6:
969 		tm.tm_year = tm.tm_year - (tm.tm_year % 100);
970 		tm.tm_year += ul / 10000;
971 		ul = ul % 10000;
972 		/* FALLTHROUGH */
973 	case 4:
974 		tm.tm_mon = (ul / 100) - 1;
975 		ul = ul % 100;
976 		/* FALLTHROUGH */
977 	case 2:
978 		tm.tm_mday = ul;
979 		/* FALLTHROUGH */
980 	case 0:
981 		break;
982 	default:
983 		return ((time_t)-1);
984 	}
985 
986 	/* Sanity check */
987 	if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12 ||
988 	    tm.tm_mday < 1 || tm.tm_mday > 31)
989 		return ((time_t)-1);
990 
991 	if (*t != '\0') {
992 		s = ++t;
993 		ul = strtoul(s, &t, 10);
994 		if (*t != '\0' && !isspace(*t))
995 			return ((time_t)-1);
996 
997 		switch (t - s) {
998 		case 6:
999 			tm.tm_sec = ul % 100;
1000 			ul /= 100;
1001 			/* FALLTHROUGH */
1002 		case 4:
1003 			tm.tm_min = ul % 100;
1004 			ul /= 100;
1005 			/* FALLTHROUGH */
1006 		case 2:
1007 			tm.tm_hour = ul;
1008 			/* FALLTHROUGH */
1009 		case 0:
1010 			break;
1011 		default:
1012 			return ((time_t)-1);
1013 		}
1014 
1015 		/* Sanity check */
1016 		if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0 ||
1017 		    tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23)
1018 			return ((time_t)-1);
1019 	}
1020 
1021 	return (mktime(&tm));
1022 }
1023