xref: /openbsd/usr.bin/newsyslog/newsyslog.c (revision 4bdff4be)
1 /*	$OpenBSD: newsyslog.c,v 1.113 2023/03/08 04:43:12 guenther Exp $	*/
2 
3 /*
4  * Copyright (c) 1999, 2002, 2003 Todd C. Miller <millert@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  *
18  * Sponsored in part by the Defense Advanced Research Projects
19  * Agency (DARPA) and Air Force Research Laboratory, Air Force
20  * Materiel Command, USAF, under agreement number F39502-99-1-0512.
21  */
22 
23 /*
24  * Copyright (c) 1997, Jason Downs.  All rights reserved.
25  *
26  * Redistribution and use in source and binary forms, with or without
27  * modification, are permitted provided that the following conditions
28  * are met:
29  * 1. Redistributions of source code must retain the above copyright
30  *    notice, this list of conditions and the following disclaimer.
31  * 2. Redistributions in binary form must reproduce the above copyright
32  *    notice, this list of conditions and the following disclaimer in the
33  *    documentation and/or other materials provided with the distribution.
34  *
35  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS
36  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
37  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38  * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT,
39  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
40  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
41  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
42  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
43  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
44  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
45  * SUCH DAMAGE.
46  */
47 
48 /*
49  * This file contains changes from the Open Software Foundation.
50  */
51 
52 /*
53  * Copyright 1988, 1989 by the Massachusetts Institute of Technology
54  *
55  * Permission to use, copy, modify, and distribute this software
56  * and its documentation for any purpose and without fee is
57  * hereby granted, provided that the above copyright notice
58  * appear in all copies and that both that copyright notice and
59  * this permission notice appear in supporting documentation,
60  * and that the names of M.I.T. and the M.I.T. S.I.P.B. not be
61  * used in advertising or publicity pertaining to distribution
62  * of the software without specific, written prior permission.
63  * M.I.T. and the M.I.T. S.I.P.B. make no representations about
64  * the suitability of this software for any purpose.  It is
65  * provided "as is" without express or implied warranty.
66  */
67 
68 /*
69  *      newsyslog - roll over selected logs at the appropriate time,
70  *              keeping the specified number of backup files around.
71  *
72  */
73 
74 #define CONF "/etc/newsyslog.conf"
75 #define PIDFILE "/var/run/syslog.pid"
76 #define COMPRESS "/usr/bin/gzip"
77 #define COMPRESS_POSTFIX ".gz"
78 #define STATS_DIR "/var/run"
79 #define SENDMAIL "/usr/sbin/sendmail"
80 
81 #include <sys/param.h>	/* DEV_BSIZE */
82 #include <sys/queue.h>
83 #include <sys/stat.h>
84 #include <sys/time.h>
85 #include <sys/wait.h>
86 
87 #include <ctype.h>
88 #include <err.h>
89 #include <errno.h>
90 #include <fcntl.h>
91 #include <grp.h>
92 #include <limits.h>
93 #include <pwd.h>
94 #include <signal.h>
95 #include <stdio.h>
96 #include <stdlib.h>
97 #include <string.h>
98 #include <time.h>
99 #include <unistd.h>
100 
101 #define CE_ROTATED	0x01		/* Log file has been rotated */
102 #define CE_COMPACT	0x02		/* Compact the archived log files */
103 #define CE_BINARY	0x04		/* Logfile is in binary, don't add */
104 					/* status messages */
105 #define CE_MONITOR	0x08		/* Monitor for changes */
106 #define CE_FOLLOW	0x10		/* Follow symbolic links */
107 #define CE_TRIMAT	0x20		/* Trim at a specific time */
108 
109 #define	MIN_PID		2		/* Don't touch pids lower than this */
110 #define	MIN_SIZE	256		/* Don't rotate if smaller (in bytes) */
111 
112 #define	DPRINTF(x)	do { if (verbose) printf x ; } while (0)
113 
114 struct conf_entry {
115 	char    *log;		/* Name of the log */
116 	char    *logbase;	/* Basename of the log */
117 	char    *backdir;	/* Directory in which to store backups */
118 	uid_t   uid;		/* Owner of log */
119 	gid_t   gid;		/* Group of log */
120 	int     numlogs;	/* Number of logs to keep */
121 	off_t   size;		/* Size cutoff to trigger trimming the log */
122 	int     hours;		/* Hours between log trimming */
123 	time_t  trim_at;	/* Specific time at which to do trimming */
124 	mode_t  permissions;	/* File permissions on the log */
125 	int	signal;		/* Signal to send (defaults to SIGHUP) */
126 	int     flags;		/* Flags (CE_COMPACT & CE_BINARY)  */
127 	char	*whom;		/* Whom to notify if logfile changes */
128 	char	*pidfile;	/* Path to file containing pid to signal */
129 	char	*runcmd;	/* Command to run instead of sending a signal */
130 	TAILQ_ENTRY(conf_entry) next;
131 };
132 TAILQ_HEAD(entrylist, conf_entry);
133 
134 struct pidinfo {
135 	char	*file;
136 	int	signal;
137 };
138 
139 int	verbose = 0;		/* Print out what's going on */
140 int	needroot = 1;		/* Root privs are necessary */
141 int	noaction = 0;		/* Don't do anything, just show it */
142 int	monitormode = 0;	/* Don't do monitoring by default */
143 int	force = 0;		/* Force the logs to be rotated */
144 char	*conf = CONF;		/* Configuration file to use */
145 time_t	timenow;
146 char	hostname[HOST_NAME_MAX+1]; /* Hostname */
147 char	daytime[33];		/* timenow in human readable form */
148 char	*arcdir;		/* Dir to put archives in (if it exists) */
149 
150 char   *lstat_log(char *, size_t, int);
151 char   *missing_field(char *, char *, int);
152 char   *sob(char *);
153 char   *son(char *);
154 int	age_old_log(struct conf_entry *);
155 int	domonitor(struct conf_entry *);
156 int	isnumberstr(char *);
157 int	log_trim(char *);
158 int	movefile(char *, char *, uid_t, gid_t, mode_t);
159 int	stat_suffix(char *, size_t, char *, struct stat *,
160 	    int (*)(const char *, struct stat *));
161 off_t	sizefile(struct stat *);
162 int	parse_file(struct entrylist *, int *);
163 time_t	parse8601(char *);
164 time_t	parseDWM(char *);
165 void	child_killer(int);
166 void	compress_log(struct conf_entry *);
167 void	do_entry(struct conf_entry *);
168 void	dotrim(struct conf_entry *);
169 void	rotate(struct conf_entry *, const char *);
170 void	parse_args(int, char **);
171 void	run_command(char *);
172 void	send_signal(char *, int);
173 void	usage(void);
174 
175 int
176 main(int argc, char **argv)
177 {
178 	struct entrylist config, runlist;
179 	struct conf_entry *p, *q, *tmp;
180 	struct pidinfo *pidlist, *pl;
181 	int status, listlen, ret;
182 	char **av;
183 
184 	parse_args(argc, argv);
185 	argc -= optind;
186 	argv += optind;
187 
188 	if (needroot && getuid() && geteuid())
189 		errx(1, "You must be root.");
190 
191 	TAILQ_INIT(&config);
192 	TAILQ_INIT(&runlist);
193 
194 	/* Keep passwd and group files open for faster lookups. */
195 	setpassent(1);
196 	setgroupent(1);
197 
198 	ret = parse_file(&config, &listlen);
199 	if (argc == 0)
200 		TAILQ_CONCAT(&runlist, &config, next);
201 	else {
202 		/* Only rotate specified files. */
203 		listlen = 0;
204 		for (av = argv; *av; av++) {
205 			TAILQ_FOREACH_SAFE(q, &config, next, tmp)
206 				if (strcmp(*av, q->log) == 0) {
207 					TAILQ_REMOVE(&config, q, next);
208 					TAILQ_INSERT_TAIL(&runlist, q, next);
209 					listlen++;
210 					break;
211 				}
212 			if (q == NULL)
213 				warnx("%s: %s not found", conf, *av);
214 		}
215 		if (TAILQ_EMPTY(&runlist))
216 			errx(1, "%s: no specified log files", conf);
217 	}
218 
219 	pidlist = calloc(listlen + 1, sizeof(struct pidinfo));
220 	if (pidlist == NULL)
221 		err(1, NULL);
222 
223 	signal(SIGCHLD, child_killer);
224 
225 	/* Step 1, rotate all log files */
226 	TAILQ_FOREACH(q, &runlist, next)
227 		do_entry(q);
228 
229 	/* Step 2, make a list of unique pid files */
230 	pl = pidlist;
231 	TAILQ_FOREACH(q, &runlist, next) {
232 		if (q->flags & CE_ROTATED) {
233 			struct pidinfo *pltmp;
234 
235 			for (pltmp = pidlist; pltmp < pl; pltmp++) {
236 				if ((q->pidfile && pltmp->file &&
237 				    strcmp(pltmp->file, q->pidfile) == 0 &&
238 				    pltmp->signal == q->signal) ||
239 				    (q->runcmd && pltmp->file &&
240 				    strcmp(q->runcmd, pltmp->file) == 0))
241 					break;
242 			}
243 			if (pltmp == pl) {	/* unique entry */
244 				if (q->runcmd) {
245 					pl->file = q->runcmd;
246 					pl->signal = -1;
247 				} else {
248 					pl->file = q->pidfile;
249 					pl->signal = q->signal;
250 				}
251 				pl++;
252 			}
253 		}
254 	}
255 
256 	/* Step 3, send a signal or run a command */
257 	for (pl--; pl >= pidlist; pl--) {
258 		if (pl->file != NULL) {
259 			if (pl->signal == -1)
260 				run_command(pl->file);
261 			else
262 				send_signal(pl->file, pl->signal);
263 		}
264 	}
265 	if (!noaction)
266 		sleep(5);
267 
268 	/* Step 4, compress the log.0 file if configured to do so and free */
269 	TAILQ_FOREACH(p, &runlist, next) {
270 		if ((p->flags & CE_COMPACT) && (p->flags & CE_ROTATED) &&
271 		    p->numlogs > 0)
272 			compress_log(p);
273 	}
274 
275 	/* Wait for children to finish, then exit */
276 	while (waitpid(-1, &status, 0) != -1)
277 		;
278 	return (ret);
279 }
280 
281 void
282 do_entry(struct conf_entry *ent)
283 {
284 	struct stat sb;
285 	int modhours;
286 	off_t size;
287 
288 	if (lstat(ent->log, &sb) != 0)
289 		return;
290 	if (!S_ISREG(sb.st_mode) &&
291 	    (!S_ISLNK(sb.st_mode) || !(ent->flags & CE_FOLLOW))) {
292 		DPRINTF(("--> not a regular file, skipping\n"));
293 		return;
294 	}
295 	if (S_ISLNK(sb.st_mode) && stat(ent->log, &sb) != 0) {
296 		DPRINTF(("--> link target does not exist, skipping\n"));
297 		return;
298 	}
299 	if (ent->uid == (uid_t)-1)
300 		ent->uid = sb.st_uid;
301 	if (ent->gid == (gid_t)-1)
302 		ent->gid = sb.st_gid;
303 
304 	DPRINTF(("%s <%d%s%s%s%s>: ", ent->log, ent->numlogs,
305 	    (ent->flags & CE_COMPACT) ? "Z" : "",
306 	    (ent->flags & CE_BINARY) ? "B" : "",
307 	    (ent->flags & CE_FOLLOW) ? "F" : "",
308 	    (ent->flags & CE_MONITOR) && monitormode ? "M" : ""));
309 	size = sizefile(&sb);
310 	modhours = age_old_log(ent);
311 	if (ent->flags & CE_TRIMAT && !force) {
312 		if (timenow < ent->trim_at ||
313 		    difftime(timenow, ent->trim_at) >= 60 * 60) {
314 			DPRINTF(("--> will trim at %s",
315 			    ctime(&ent->trim_at)));
316 			return;
317 		} else if (ent->hours <= 0) {
318 			DPRINTF(("--> time is up\n"));
319 		}
320 	}
321 	if (ent->size > 0)
322 		DPRINTF(("size (KB): %.2f [%d] ", size / 1024.0,
323 		    (int)(ent->size / 1024)));
324 	if (ent->hours > 0)
325 		DPRINTF(("age (hr): %d [%d] ", modhours, ent->hours));
326 	if (monitormode && (ent->flags & CE_MONITOR) && domonitor(ent))
327 		DPRINTF(("--> monitored\n"));
328 	else if (!monitormode &&
329 	    (force || (ent->size > 0 && size >= ent->size) ||
330 	    (ent->hours <= 0 && (ent->flags & CE_TRIMAT)) ||
331 	    (ent->hours > 0 && (modhours >= ent->hours || modhours < 0)
332 	    && ((ent->flags & CE_BINARY) || size >= MIN_SIZE)))) {
333 		DPRINTF(("--> trimming log....\n"));
334 		if (noaction && !verbose)
335 			printf("%s <%d%s%s%s>\n", ent->log,
336 			    ent->numlogs,
337 			    (ent->flags & CE_COMPACT) ? "Z" : "",
338 			    (ent->flags & CE_BINARY) ? "B" : "",
339 			    (ent->flags & CE_FOLLOW) ? "F" : "");
340 		dotrim(ent);
341 		ent->flags |= CE_ROTATED;
342 	} else
343 		DPRINTF(("--> skipping\n"));
344 }
345 
346 /* Run the specified command */
347 void
348 run_command(char *cmd)
349 {
350 	if (noaction)
351 		(void)printf("run %s\n", cmd);
352 	else
353 		system(cmd);
354 }
355 
356 /* Send a signal to the pid specified by pidfile */
357 void
358 send_signal(char *pidfile, int signal)
359 {
360 	char line[BUFSIZ], *ep, *err;
361 	pid_t pid;
362 	long lval;
363 	FILE *f;
364 
365 	if ((f = fopen(pidfile, "r")) == NULL) {
366 		warn("can't open %s", pidfile);
367 		return;
368 	}
369 
370 	pid = 0;
371 	errno = 0;
372 	err = NULL;
373 	if (fgets(line, sizeof(line), f)) {
374 		lval = strtol(line, &ep, 10);
375 		if (line[0] == '\0' || (*ep != '\0' && *ep != '\n'))
376 			err = "invalid number in";
377 		else if (lval < 0 || (errno == ERANGE && lval == LONG_MAX))
378 			err = "out of range number in";
379 		else if (lval == 0)
380 			err = "no number in";
381 		else if (lval < MIN_PID)
382 			err = "preposterous process number in";
383 		else
384 			pid = (pid_t)lval;
385 	} else {
386 		if (errno == 0)
387 			err = "empty";
388 		else
389 			err = "error reading";
390 	}
391 	(void)fclose(f);
392 
393 	if (err)
394 		warnx("%s pid file: %s", err, pidfile);
395 	else if (noaction)
396 		(void)printf("kill -%s %ld\n", sys_signame[signal], (long)pid);
397 	else if (kill(pid, signal))
398 		warnx("warning - could not send SIG%s to PID from pid file %s",
399 		    sys_signame[signal], pidfile);
400 }
401 
402 void
403 parse_args(int argc, char **argv)
404 {
405 	struct timeval now;
406 	struct tm *tm;
407 	size_t l;
408 	char *p;
409 	int ch;
410 
411 	gettimeofday(&now, NULL);
412 	timenow = now.tv_sec;
413 	tm = gmtime(&now.tv_sec);
414 	l = strftime(daytime, sizeof(daytime), "%FT%T", tm);
415 	snprintf(daytime + l, sizeof(daytime) - l, ".%03ldZ",
416 	    now.tv_usec / 1000);
417 
418 	/* Let's get our hostname */
419 	(void)gethostname(hostname, sizeof(hostname));
420 
421 	/* Truncate domain */
422 	if ((p = strchr(hostname, '.')) != NULL)
423 		*p = '\0';
424 
425 	while ((ch = getopt(argc, argv, "Fmnrva:f:")) != -1) {
426 		switch (ch) {
427 		case 'a':
428 			arcdir = optarg;
429 			break;
430 		case 'n':
431 			noaction = 1;	/* This implies needroot as off */
432 			/* fall through */
433 		case 'r':
434 			needroot = 0;
435 			break;
436 		case 'v':
437 			verbose = 1;
438 			break;
439 		case 'f':
440 			conf = optarg;
441 			break;
442 		case 'm':
443 			monitormode = 1;
444 			break;
445 		case 'F':
446 			force = 1;
447 			break;
448 		default:
449 			usage();
450 		}
451 	}
452 	if (monitormode && force)
453 		errx(1, "cannot specify both -m and -F flags");
454 }
455 
456 void
457 usage(void)
458 {
459 	extern const char *__progname;
460 
461 	(void)fprintf(stderr, "usage: %s [-Fmnrv] [-a directory] "
462 	    "[-f config_file] [log ...]\n", __progname);
463 	exit(1);
464 }
465 
466 /*
467  * Parse a configuration file and build a linked list of all the logs
468  * to process
469  */
470 int
471 parse_file(struct entrylist *list, int *nentries)
472 {
473 	char line[BUFSIZ], *parse, *q, *errline, *group, *tmp, *ep;
474 	struct conf_entry *working;
475 	struct stat sb;
476 	int lineno = 0;
477 	int ret = 0;
478 	FILE *f;
479 	long l;
480 
481 	if (strcmp(conf, "-") == 0)
482 		f = stdin;
483 	else if ((f = fopen(conf, "r")) == NULL)
484 		err(1, "can't open %s", conf);
485 
486 	*nentries = 0;
487 
488 nextline:
489 	while (fgets(line, sizeof(line), f) != NULL) {
490 		lineno++;
491 		tmp = sob(line);
492 		if (*tmp == '\0' || *tmp == '#')
493 			continue;
494 		errline = strdup(tmp);
495 		if (errline == NULL)
496 			err(1, NULL);
497 		working = calloc(1, sizeof(*working));
498 		if (working == NULL)
499 			err(1, NULL);
500 
501 		q = parse = missing_field(sob(line), errline, lineno);
502 		*(parse = son(line)) = '\0';
503 		working->log = strdup(q);
504 		if (working->log == NULL)
505 			err(1, NULL);
506 
507 		if ((working->logbase = strrchr(working->log, '/')) != NULL)
508 			working->logbase++;
509 
510 		q = parse = missing_field(sob(++parse), errline, lineno);
511 		*(parse = son(parse)) = '\0';
512 		if ((group = strchr(q, ':')) != NULL ||
513 		    (group = strrchr(q, '.')) != NULL)  {
514 			*group++ = '\0';
515 			if (*q == '\0') {
516 				working->uid = (uid_t)-1;
517 			} else if (isnumberstr(q)) {
518 				working->uid = atoi(q);
519 			} else if (uid_from_user(q, &working->uid) == -1) {
520 				warnx("%s:%d: unknown user %s --> skipping",
521 				    conf, lineno, q);
522 				ret = 1;
523 				goto nextline;
524 			}
525 
526 			q = group;
527 			if (*q == '\0') {
528 				working->gid = (gid_t)-1;
529 			} else if (isnumberstr(q)) {
530 				working->gid = atoi(q);
531 			} else if (gid_from_group(q, &working->gid) == -1) {
532 				warnx("%s:%d: unknown group %s --> skipping",
533 				    conf, lineno, q);
534 				ret = 1;
535 				goto nextline;
536 			}
537 
538 			q = parse = missing_field(sob(++parse), errline, lineno);
539 			*(parse = son(parse)) = '\0';
540 		} else {
541 			working->uid = (uid_t)-1;
542 			working->gid = (gid_t)-1;
543 		}
544 
545 		l = strtol(q, &ep, 8);
546 		if (*ep != '\0' || l < 0 || l > ALLPERMS) {
547 			warnx("%s:%d: bad permissions: %s --> skipping", conf,
548 			    lineno, q);
549 			ret = 1;
550 			goto nextline;
551 		}
552 		working->permissions = (mode_t)l;
553 
554 		q = parse = missing_field(sob(++parse), errline, lineno);
555 		*(parse = son(parse)) = '\0';
556 		l = strtol(q, &ep, 10);
557 		if (*ep != '\0' || l < 0 || l >= INT_MAX) {
558 			warnx("%s:%d: bad number: %s --> skipping", conf,
559 			    lineno, q);
560 			ret = 1;
561 			goto nextline;
562 		}
563 		working->numlogs = (int)l;
564 
565 		q = parse = missing_field(sob(++parse), errline, lineno);
566 		*(parse = son(parse)) = '\0';
567 		if (isdigit((unsigned char)*q))
568 			working->size = atoi(q) * 1024;
569 		else
570 			working->size = -1;
571 
572 		working->flags = 0;
573 		q = parse = missing_field(sob(++parse), errline, lineno);
574 		*(parse = son(parse)) = '\0';
575 		l = strtol(q, &ep, 10);
576 		if (l < 0 || l >= INT_MAX) {
577 			warnx("%s:%d: interval out of range: %s --> skipping",
578 			    conf, lineno, q);
579 			ret = 1;
580 			goto nextline;
581 		}
582 		working->hours = (int)l;
583 		switch (*ep) {
584 		case '\0':
585 			break;
586 		case '@':
587 			working->trim_at = parse8601(ep + 1);
588 			if (working->trim_at == (time_t) - 1) {
589 				warnx("%s:%d: bad time: %s --> skipping", conf,
590 				    lineno, q);
591 				ret = 1;
592 				goto nextline;
593 			}
594 			working->flags |= CE_TRIMAT;
595 			break;
596 		case '$':
597 			working->trim_at = parseDWM(ep + 1);
598 			if (working->trim_at == (time_t) - 1) {
599 				warnx("%s:%d: bad time: %s --> skipping", conf,
600 				    lineno, q);
601 				ret = 1;
602 				goto nextline;
603 			}
604 			working->flags |= CE_TRIMAT;
605 			break;
606 		case '*':
607 			if (q == ep)
608 				break;
609 			/* FALLTHROUGH */
610 		default:
611 			warnx("%s:%d: bad interval/at: %s --> skipping", conf,
612 			    lineno, q);
613 			ret = 1;
614 			goto nextline;
615 		}
616 
617 		q = sob(++parse);	/* Optional field */
618 		if (*q == 'Z' || *q == 'z' || *q == 'B' || *q == 'b' ||
619 		    *q == 'M' || *q == 'm') {
620 			*(parse = son(q)) = '\0';
621 			while (*q) {
622 				switch (*q) {
623 				case 'Z':
624 				case 'z':
625 					working->flags |= CE_COMPACT;
626 					break;
627 				case 'B':
628 				case 'b':
629 					working->flags |= CE_BINARY;
630 					break;
631 				case 'M':
632 				case 'm':
633 					working->flags |= CE_MONITOR;
634 					break;
635 				case 'F':
636 				case 'f':
637 					working->flags |= CE_FOLLOW;
638 					break;
639 				default:
640 					warnx("%s:%d: illegal flag: `%c'"
641 					    " --> skipping",
642 					    conf, lineno, *q);
643 					ret = 1;
644 					goto nextline;
645 				}
646 				q++;
647 			}
648 		} else
649 			parse--;	/* no flags so undo */
650 
651 		working->pidfile = PIDFILE;
652 		working->signal = SIGHUP;
653 		working->runcmd = NULL;
654 		working->whom = NULL;
655 		for (;;) {
656 			q = parse = sob(++parse);	/* Optional field */
657 			if (q == NULL || *q == '\0')
658 				break;
659 			if (*q == '/') {
660 				*(parse = son(parse)) = '\0';
661 				if (strlen(q) >= PATH_MAX) {
662 					warnx("%s:%d: pathname too long: %s"
663 					    " --> skipping",
664 					    conf, lineno, q);
665 					ret = 1;
666 					goto nextline;
667 				}
668 				working->pidfile = strdup(q);
669 				if (working->pidfile == NULL)
670 					err(1, NULL);
671 			} else if (*q == '"' && (tmp = strchr(q + 1, '"'))) {
672 				*(parse = tmp) = '\0';
673 				if (*++q != '\0') {
674 					working->runcmd = strdup(q);
675 					if (working->runcmd == NULL)
676 						err(1, NULL);
677 				}
678 				working->pidfile = NULL;
679 				working->signal = -1;
680 			} else if (strncmp(q, "SIG", 3) == 0) {
681 				int i;
682 
683 				*(parse = son(parse)) = '\0';
684 				for (i = 1; i < NSIG; i++) {
685 					if (!strcmp(sys_signame[i], q + 3)) {
686 						working->signal = i;
687 						break;
688 					}
689 				}
690 				if (i == NSIG) {
691 					warnx("%s:%d: unknown signal: %s"
692 					    " --> skipping",
693 					    conf, lineno, q);
694 					ret = 1;
695 					goto nextline;
696 				}
697 			} else if (working->flags & CE_MONITOR) {
698 				*(parse = son(parse)) = '\0';
699 				working->whom = strdup(q);
700 				if (working->whom == NULL)
701 					err(1, NULL);
702 			} else {
703 				warnx("%s:%d: unrecognized field: %s"
704 				    " --> skipping",
705 				    conf, lineno, q);
706 				ret = 1;
707 				goto nextline;
708 			}
709 		}
710 		free(errline);
711 
712 		if ((working->flags & CE_MONITOR) && working->whom == NULL) {
713 			warnx("%s:%d: missing monitor notification field"
714 			    " --> skipping",
715 			    conf, lineno);
716 			ret = 1;
717 			goto nextline;
718 		}
719 
720 		/* If there is an arcdir, set working->backdir. */
721 		if (arcdir != NULL && working->logbase != NULL) {
722 			if (*arcdir == '/') {
723 				/* Fully qualified arcdir */
724 				working->backdir = arcdir;
725 			} else {
726 				/* arcdir is relative to log's parent dir */
727 				*(working->logbase - 1) = '\0';
728 				if ((asprintf(&working->backdir, "%s/%s",
729 				    working->log, arcdir)) == -1)
730 					err(1, NULL);
731 				*(working->logbase - 1) = '/';
732 			}
733 			/* Ignore arcdir if it doesn't exist. */
734 			if (stat(working->backdir, &sb) != 0 ||
735 			    !S_ISDIR(sb.st_mode)) {
736 				if (working->backdir != arcdir)
737 					free(working->backdir);
738 				working->backdir = NULL;
739 			}
740 		} else
741 			working->backdir = NULL;
742 
743 		/* Make sure we can't oflow PATH_MAX */
744 		if (working->backdir != NULL) {
745 			if (snprintf(line, sizeof(line), "%s/%s.%d%s",
746 			    working->backdir, working->logbase,
747 			    working->numlogs, COMPRESS_POSTFIX) >= PATH_MAX) {
748 				warnx("%s:%d: pathname too long: %s"
749 				    " --> skipping", conf, lineno, q);
750 				ret = 1;
751 				goto nextline;
752 			}
753 		} else {
754 			if (snprintf(line, sizeof(line), "%s.%d%s",
755 			    working->log, working->numlogs, COMPRESS_POSTFIX)
756 			    >= PATH_MAX) {
757 				warnx("%s:%d: pathname too long: %s"
758 				    " --> skipping", conf, lineno,
759 				    working->log);
760 				ret = 1;
761 				goto nextline;
762 			}
763 		}
764 		TAILQ_INSERT_TAIL(list, working, next);
765 		(*nentries)++;
766 	}
767 	(void)fclose(f);
768 	return (ret);
769 }
770 
771 char *
772 missing_field(char *p, char *errline, int lineno)
773 {
774 	if (p == NULL || *p == '\0') {
775 		warnx("%s:%d: missing field", conf, lineno);
776 		fputs(errline, stderr);
777 		exit(1);
778 	}
779 	return (p);
780 }
781 
782 void
783 rotate(struct conf_entry *ent, const char *oldlog)
784 {
785 	char file1[PATH_MAX], file2[PATH_MAX], *suffix;
786 	int numdays = ent->numlogs - 1;
787 	int done = 0;
788 
789 	/* Remove old logs */
790 	do {
791 		(void)snprintf(file1, sizeof(file1), "%s.%d", oldlog, numdays);
792 		(void)snprintf(file2, sizeof(file2), "%s.%d%s", oldlog,
793 		    numdays, COMPRESS_POSTFIX);
794 		if (noaction) {
795 			printf("\trm -f %s %s\n", file1, file2);
796 			done = access(file1, 0) && access(file2, 0);
797 		} else {
798 			done = unlink(file1) && unlink(file2);
799 		}
800 		numdays++;
801 	} while (done == 0);
802 
803 	/* Move down log files */
804 	for (numdays = ent->numlogs - 2; numdays >= 0; numdays--) {
805 		/*
806 		 * If both the compressed archive and the non-compressed archive
807 		 * exist, we decide which to rotate based on the CE_COMPACT flag
808 		 */
809 		(void)snprintf(file1, sizeof(file1), "%s.%d", oldlog, numdays);
810 		suffix = lstat_log(file1, sizeof(file1), ent->flags);
811 		if (suffix == NULL)
812 			continue;
813 		(void)snprintf(file2, sizeof(file2), "%s.%d%s", oldlog,
814 		    numdays + 1, suffix);
815 
816 		if (noaction) {
817 			printf("\tmv %s %s\n", file1, file2);
818 			printf("\tchmod %o %s\n", ent->permissions, file2);
819 			printf("\tchown %u:%u %s\n", ent->uid, ent->gid, file2);
820 		} else {
821 			if (rename(file1, file2))
822 				warn("can't mv %s to %s", file1, file2);
823 			if (chmod(file2, ent->permissions))
824 				warn("can't chmod %s", file2);
825 			if (chown(file2, ent->uid, ent->gid))
826 				warn("can't chown %s", file2);
827 		}
828 	}
829 }
830 
831 void
832 dotrim(struct conf_entry *ent)
833 {
834 	char file1[PATH_MAX], file2[PATH_MAX], oldlog[PATH_MAX];
835 	int fd;
836 
837 	/* Is there a separate backup dir? */
838 	if (ent->backdir != NULL)
839 		snprintf(oldlog, sizeof(oldlog), "%s/%s", ent->backdir,
840 		    ent->logbase);
841 	else
842 		strlcpy(oldlog, ent->log, sizeof(oldlog));
843 
844 	if (ent->numlogs > 0)
845 		rotate(ent, oldlog);
846 	if (!noaction && !(ent->flags & CE_BINARY))
847 		(void)log_trim(ent->log);
848 
849 	(void)snprintf(file2, sizeof(file2), "%s.XXXXXXXXXX", ent->log);
850 	if (noaction)  {
851 		printf("\tmktemp %s\n", file2);
852 	} else {
853 		if ((fd = mkstemp(file2)) == -1)
854 			err(1, "can't start '%s' log", file2);
855 		if (fchmod(fd, ent->permissions))
856 			err(1, "can't chmod '%s' log file", file2);
857 		if (fchown(fd, ent->uid, ent->gid))
858 			err(1, "can't chown '%s' log file", file2);
859 		(void)close(fd);
860 		/* Add status message */
861 		if (!(ent->flags & CE_BINARY) && log_trim(file2))
862 			err(1, "can't add status message to log '%s'", file2);
863 	}
864 
865 	if (ent->numlogs == 0) {
866 		if (noaction)
867 			printf("\trm %s\n", ent->log);
868 		else if (unlink(ent->log))
869 			warn("can't rm %s", ent->log);
870 	} else {
871 		(void)snprintf(file1, sizeof(file1), "%s.0", oldlog);
872 		if (noaction) {
873 			printf("\tmv %s to %s\n", ent->log, file1);
874 			printf("\tchmod %o %s\n", ent->permissions, file1);
875 			printf("\tchown %u:%u %s\n", ent->uid, ent->gid, file1);
876 		} else if (movefile(ent->log, file1, ent->uid, ent->gid,
877 		    ent->permissions))
878 			warn("can't mv %s to %s", ent->log, file1);
879 	}
880 
881 	/* Now move the new log file into place */
882 	if (noaction)
883 		printf("\tmv %s to %s\n", file2, ent->log);
884 	else if (rename(file2, ent->log))
885 		warn("can't mv %s to %s", file2, ent->log);
886 }
887 
888 /* Log the fact that the logs were turned over */
889 int
890 log_trim(char *log)
891 {
892 	FILE    *f;
893 
894 	if ((f = fopen(log, "a")) == NULL)
895 		return (-1);
896 	(void)fprintf(f, "%s %s newsyslog[%ld]: logfile turned over\n",
897 	    daytime, hostname, (long)getpid());
898 	if (fclose(f) == EOF)
899 		err(1, "log_trim: fclose");
900 	return (0);
901 }
902 
903 /* Fork off compress or gzip to compress the old log file */
904 void
905 compress_log(struct conf_entry *ent)
906 {
907 	char *base, tmp[PATH_MAX];
908 	pid_t pid;
909 
910 	if (ent->backdir != NULL)
911 		snprintf(tmp, sizeof(tmp), "%s/%s.0", ent->backdir,
912 		    ent->logbase);
913 	else
914 		snprintf(tmp, sizeof(tmp), "%s.0", ent->log);
915 
916 	if ((base = strrchr(COMPRESS, '/')) == NULL)
917 		base = COMPRESS;
918 	else
919 		base++;
920 	if (noaction) {
921 		printf("%s %s\n", base, tmp);
922 		return;
923 	}
924 	pid = fork();
925 	if (pid == -1) {
926 		err(1, "fork");
927 	} else if (pid == 0) {
928 		(void)execl(COMPRESS, base, "-f", tmp, (char *)NULL);
929 		warn(COMPRESS);
930 		_exit(1);
931 	}
932 }
933 
934 /* Return size in bytes of a file */
935 off_t
936 sizefile(struct stat *sb)
937 {
938 	/* For sparse files, return the size based on number of blocks used. */
939 	if (sb->st_size / DEV_BSIZE > sb->st_blocks)
940 		return (sb->st_blocks * DEV_BSIZE);
941 	else
942 		return (sb->st_size);
943 }
944 
945 /* Return the age (in hours) of old log file (file.0), or -1 if none */
946 int
947 age_old_log(struct conf_entry *ent)
948 {
949 	char file[PATH_MAX];
950 	struct stat sb;
951 
952 	if (ent->backdir != NULL)
953 		(void)snprintf(file, sizeof(file), "%s/%s.0", ent->backdir,
954 		    ent->logbase);
955 	else
956 		(void)snprintf(file, sizeof(file), "%s.0", ent->log);
957 	if (ent->flags & CE_COMPACT) {
958 		if (stat_suffix(file, sizeof(file), COMPRESS_POSTFIX, &sb,
959 		    stat) < 0 && stat(file, &sb) == -1)
960 			return (-1);
961 	} else {
962 		if (stat(file, &sb) == -1 && stat_suffix(file, sizeof(file),
963 		    COMPRESS_POSTFIX, &sb, stat) < 0)
964 			return (-1);
965 	}
966 	return ((int)(timenow - sb.st_mtime + 1800) / 3600);
967 }
968 
969 /* Skip Over Blanks */
970 char *
971 sob(char *p)
972 {
973 	if (p == NULL)
974 		return(p);
975 	while (isspace((unsigned char)*p))
976 		p++;
977 	return (p);
978 }
979 
980 /* Skip Over Non-Blanks */
981 char *
982 son(char *p)
983 {
984 	while (p && *p && !isspace((unsigned char)*p))
985 		p++;
986 	return (p);
987 }
988 
989 /* Check if string is actually a number */
990 int
991 isnumberstr(char *string)
992 {
993 	while (*string) {
994 		if (!isdigit((unsigned char)*string++))
995 			return (0);
996 	}
997 	return (1);
998 }
999 
1000 int
1001 domonitor(struct conf_entry *ent)
1002 {
1003 	char fname[PATH_MAX], *flog, *p, *rb = NULL;
1004 	struct stat sb, tsb;
1005 	off_t osize;
1006 	FILE *fp;
1007 	int rd;
1008 
1009 	if (stat(ent->log, &sb) == -1)
1010 		return (0);
1011 
1012 	if (noaction) {
1013 		if (!verbose)
1014 			printf("%s: monitored\n", ent->log);
1015 		return (1);
1016 	}
1017 
1018 	flog = strdup(ent->log);
1019 	if (flog == NULL)
1020 		err(1, NULL);
1021 
1022 	for (p = flog; *p != '\0'; p++) {
1023 		if (*p == '/')
1024 			*p = '_';
1025 	}
1026 	snprintf(fname, sizeof(fname), "%s/newsyslog.%s.size",
1027 	    STATS_DIR, flog);
1028 
1029 	/* ..if it doesn't exist, simply record the current size. */
1030 	if ((sb.st_size == 0) || stat(fname, &tsb) == -1)
1031 		goto update;
1032 
1033 	fp = fopen(fname, "r");
1034 	if (fp == NULL) {
1035 		warn("%s", fname);
1036 		goto cleanup;
1037 	}
1038 	if (fscanf(fp, "%lld\n", &osize) != 1) {
1039 		fclose(fp);
1040 		goto update;
1041 	}
1042 
1043 	fclose(fp);
1044 
1045 	/* If the file is smaller, mark the entire thing as changed. */
1046 	if (sb.st_size < osize)
1047 		osize = 0;
1048 
1049 	/* Now see if current size is larger. */
1050 	if (sb.st_size > osize) {
1051 		rb = malloc(sb.st_size - osize);
1052 		if (rb == NULL)
1053 			err(1, NULL);
1054 
1055 		/* Open logfile, seek. */
1056 		fp = fopen(ent->log, "r");
1057 		if (fp == NULL) {
1058 			warn("%s", ent->log);
1059 			goto cleanup;
1060 		}
1061 		fseek(fp, osize, SEEK_SET);
1062 		rd = fread(rb, 1, sb.st_size - osize, fp);
1063 		if (rd < 1) {
1064 			warn("fread");
1065 			fclose(fp);
1066 			goto cleanup;
1067 		}
1068 		fclose(fp);
1069 
1070 		/* Send message. */
1071 		fp = popen(SENDMAIL " -t", "w");
1072 		if (fp == NULL) {
1073 			warn("popen");
1074 			goto cleanup;
1075 		}
1076 		fprintf(fp, "Auto-Submitted: auto-generated\n");
1077 		fprintf(fp, "To: %s\nSubject: LOGFILE NOTIFICATION: %s\n\n\n",
1078 		    ent->whom, ent->log);
1079 		fwrite(rb, 1, rd, fp);
1080 		fputs("\n\n", fp);
1081 
1082 		pclose(fp);
1083 	}
1084 update:
1085 	/* Reopen for writing and update file. */
1086 	fp = fopen(fname, "w");
1087 	if (fp == NULL) {
1088 		warn("%s", fname);
1089 		goto cleanup;
1090 	}
1091 	fprintf(fp, "%lld\n", (long long)sb.st_size);
1092 	fclose(fp);
1093 
1094 cleanup:
1095 	free(flog);
1096 	free(rb);
1097 	return (1);
1098 }
1099 
1100 void
1101 child_killer(int signo)
1102 {
1103 	int save_errno = errno;
1104 	int status;
1105 
1106 	while (waitpid(-1, &status, WNOHANG) > 0)
1107 		;
1108 	errno = save_errno;
1109 }
1110 
1111 int
1112 stat_suffix(char *file, size_t size, char *suffix, struct stat *sp,
1113     int (*func)(const char *, struct stat *))
1114 {
1115 	size_t n;
1116 
1117 	n = strlcat(file, suffix, size);
1118 	if (n < size && func(file, sp) == 0)
1119 		return (0);
1120 	file[n - strlen(suffix)] = '\0';
1121 	return (-1);
1122 }
1123 
1124 /*
1125  * lstat() a log, possibly appending a suffix; order is based on flags.
1126  * Returns the suffix appended (may be empty string) or NULL if no file.
1127  */
1128 char *
1129 lstat_log(char *file, size_t size, int flags)
1130 {
1131 	struct stat sb;
1132 
1133 	if (flags & CE_COMPACT) {
1134 		if (stat_suffix(file, size, COMPRESS_POSTFIX, &sb, lstat) == 0)
1135 			return (COMPRESS_POSTFIX);
1136 		if (lstat(file, &sb) == 0)
1137 			return ("");
1138 	} else {
1139 		if (lstat(file, &sb) == 0)
1140 			return ("");
1141 		if (stat_suffix(file, size, COMPRESS_POSTFIX, &sb, lstat) == 0)
1142 			return (COMPRESS_POSTFIX);
1143 
1144 	}
1145 	return (NULL);
1146 }
1147 
1148 /*
1149  * Parse a limited subset of ISO 8601. The specific format is as follows:
1150  *
1151  * [CC[YY[MM[DD]]]][THH[MM[SS]]]	(where `T' is the literal letter)
1152  *
1153  * We don't accept a timezone specification; missing fields (including timezone)
1154  * are defaulted to the current date but time zero.
1155  */
1156 time_t
1157 parse8601(char *s)
1158 {
1159 	struct tm tm, *tmp;
1160 	char *t;
1161 	long l;
1162 
1163 	tmp = localtime(&timenow);
1164 	tm = *tmp;
1165 
1166 	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1167 
1168 	l = strtol(s, &t, 10);
1169 	if (l < 0 || l >= INT_MAX || (*t != '\0' && *t != 'T'))
1170 		return (-1);
1171 
1172 	/*
1173 	 * Now t points either to the end of the string (if no time was
1174 	 * provided) or to the letter `T' which separates date and time in
1175 	 * ISO 8601.  The pointer arithmetic is the same for either case.
1176 	 */
1177 	switch (t - s) {
1178 	case 8:
1179 		tm.tm_year = ((l / 1000000) - 19) * 100;
1180 		l = l % 1000000;
1181 	case 6:
1182 		tm.tm_year -= tm.tm_year % 100;
1183 		tm.tm_year += l / 10000;
1184 		l = l % 10000;
1185 	case 4:
1186 		tm.tm_mon = (l / 100) - 1;
1187 		l = l % 100;
1188 	case 2:
1189 		tm.tm_mday = l;
1190 	case 0:
1191 		break;
1192 	default:
1193 		return (-1);
1194 	}
1195 
1196 	/* sanity check */
1197 	if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12 ||
1198 	    tm.tm_mday < 1 || tm.tm_mday > 31)
1199 		return (-1);
1200 
1201 	if (*t != '\0') {
1202 		s = ++t;
1203 		l = strtol(s, &t, 10);
1204 		if (l < 0 || l >= INT_MAX ||
1205 		    (*t != '\0' && !isspace((unsigned char)*t)))
1206 			return (-1);
1207 
1208 		switch (t - s) {
1209 		case 6:
1210 			tm.tm_sec = l % 100;
1211 			l /= 100;
1212 		case 4:
1213 			tm.tm_min = l % 100;
1214 			l /= 100;
1215 		case 2:
1216 			tm.tm_hour = l;
1217 		case 0:
1218 			break;
1219 		default:
1220 			return (-1);
1221 		}
1222 
1223 		/* sanity check */
1224 		if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0 ||
1225 		    tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23)
1226 			return (-1);
1227 	}
1228 	return (mktime(&tm));
1229 }
1230 
1231 /*-
1232  * Parse a cyclic time specification, the format is as follows:
1233  *
1234  *	[Dhh] or [Wd[Dhh]] or [Mdd[Dhh]]
1235  *
1236  * to rotate a logfile cyclic at
1237  *
1238  *	- every day (D) within a specific hour (hh)	(hh = 0...23)
1239  *	- once a week (W) at a specific day (d)     OR	(d = 0..6, 0 = Sunday)
1240  *	- once a month (M) at a specific day (d)	(d = 1..31,l|L)
1241  *
1242  * We don't accept a timezone specification; missing fields
1243  * are defaulted to the current date but time zero.
1244  */
1245 time_t
1246 parseDWM(char *s)
1247 {
1248 	static int mtab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1249 	int WMseen = 0, Dseen = 0, nd;
1250 	struct tm tm, *tmp;
1251 	char *t;
1252 	long l;
1253 
1254 	tmp = localtime(&timenow);
1255 	tm = *tmp;
1256 
1257 	/* set no. of days per month */
1258 
1259 	nd = mtab[tm.tm_mon];
1260 
1261 	if (tm.tm_mon == 1) {
1262 		if (((tm.tm_year + 1900) % 4 == 0) &&
1263 		    ((tm.tm_year + 1900) % 100 != 0) &&
1264 		    ((tm.tm_year + 1900) % 400 == 0)) {
1265 			nd++;	/* leap year, 29 days in february */
1266 		}
1267 	}
1268 	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1269 
1270 	for (;;) {
1271 		switch (*s) {
1272 		case 'D':
1273 			if (Dseen)
1274 				return (-1);
1275 			Dseen++;
1276 			s++;
1277 			l = strtol(s, &t, 10);
1278 			if (l < 0 || l > 23)
1279 				return (-1);
1280 			tm.tm_hour = l;
1281 			break;
1282 
1283 		case 'W':
1284 			if (WMseen)
1285 				return (-1);
1286 			WMseen++;
1287 			s++;
1288 			l = strtol(s, &t, 10);
1289 			if (l < 0 || l > 6)
1290 				return (-1);
1291 			if (l != tm.tm_wday) {
1292 				int save;
1293 
1294 				if (l < tm.tm_wday) {
1295 					save = 6 - tm.tm_wday;
1296 					save += (l + 1);
1297 				} else {
1298 					save = l - tm.tm_wday;
1299 				}
1300 
1301 				tm.tm_mday += save;
1302 
1303 				if (tm.tm_mday > nd) {
1304 					tm.tm_mon++;
1305 					tm.tm_mday = tm.tm_mday - nd;
1306 				}
1307 			}
1308 			break;
1309 
1310 		case 'M':
1311 			if (WMseen)
1312 				return (-1);
1313 			WMseen++;
1314 			s++;
1315 			if (tolower((unsigned char)*s) == 'l') {
1316 				tm.tm_mday = nd;
1317 				s++;
1318 				t = s;
1319 			} else {
1320 				l = strtol(s, &t, 10);
1321 				if (l < 1 || l > 31)
1322 					return (-1);
1323 
1324 				if (l > nd)
1325 					return (-1);
1326 				if (l < tm.tm_mday)
1327 					tm.tm_mon++;
1328 				tm.tm_mday = l;
1329 			}
1330 			break;
1331 
1332 		default:
1333 			return (-1);
1334 			break;
1335 		}
1336 
1337 		if (*t == '\0' || isspace((unsigned char)*t))
1338 			break;
1339 		else
1340 			s = t;
1341 	}
1342 	return (mktime(&tm));
1343 }
1344 
1345 /*
1346  * Move a file using rename(2) if possible and copying if not.
1347  */
1348 int
1349 movefile(char *from, char *to, uid_t owner_uid, gid_t group_gid, mode_t perm)
1350 {
1351 	FILE *src, *dst;
1352 	int i;
1353 
1354 	/* try rename(2) first */
1355 	if (rename(from, to) == 0) {
1356 		if (chmod(to, perm))
1357 			warn("can't chmod %s", to);
1358 		if (chown(to, owner_uid, group_gid))
1359 			warn("can't chown %s", to);
1360 		return (0);
1361 	} else if (errno != EXDEV)
1362 		return (-1);
1363 
1364 	/* different filesystem, have to copy the file */
1365 	if ((src = fopen(from, "r")) == NULL)
1366 		err(1, "can't fopen %s for reading", from);
1367 	if ((dst = fopen(to, "w")) == NULL)
1368 		err(1, "can't fopen %s for writing", to);
1369 	if (fchmod(fileno(dst), perm))
1370 		err(1, "can't fchmod %s", to);
1371 	if (fchown(fileno(dst), owner_uid, group_gid))
1372 		err(1, "can't fchown %s", to);
1373 
1374 	while ((i = getc(src)) != EOF) {
1375 		if ((putc(i, dst)) == EOF)
1376 			err(1, "error writing to %s", to);
1377 	}
1378 
1379 	if (ferror(src))
1380 		err(1, "error reading from %s", from);
1381 	if ((fclose(src)) != 0)
1382 		err(1, "can't fclose %s", from);
1383 	if ((fclose(dst)) != 0)
1384 		err(1, "can't fclose %s", to);
1385 	if ((unlink(from)) != 0)
1386 		err(1, "can't unlink %s", from);
1387 
1388 	return (0);
1389 }
1390